add-frames #1
69 changed files with 2122 additions and 119 deletions
|
@ -1,3 +1,4 @@
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
node_modules
|
node_modules
|
||||||
tailwind.config.js
|
tailwind.config.js
|
||||||
|
backend
|
||||||
|
|
|
@ -14,7 +14,6 @@ image: node:14-alpine
|
||||||
variables:
|
variables:
|
||||||
CHART_NAME: stackspin-dashboard
|
CHART_NAME: stackspin-dashboard
|
||||||
CHART_DIR: deployment/helmchart/
|
CHART_DIR: deployment/helmchart/
|
||||||
KANIKO_BUILD_IMAGENAME: dashboard
|
|
||||||
|
|
||||||
build-project:
|
build-project:
|
||||||
stage: build-project
|
stage: build-project
|
||||||
|
@ -33,16 +32,38 @@ build-project:
|
||||||
paths:
|
paths:
|
||||||
- web-build
|
- web-build
|
||||||
|
|
||||||
build-container:
|
.kaniko-build:
|
||||||
|
script:
|
||||||
|
- cd ${DIRECTORY}
|
||||||
|
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
|
||||||
|
- export CONTAINER_TAG=${CI_COMMIT_TAG:-${CI_COMMIT_REF_SLUG}}
|
||||||
|
- /kaniko/executor --cache=true --context ${CI_PROJECT_DIR}/${DIRECTORY} --destination ${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CONTAINER_TAG}
|
||||||
|
|
||||||
|
build-fontend-container:
|
||||||
stage: build-container
|
stage: build-container
|
||||||
image:
|
image:
|
||||||
# We need a shell to provide the registry credentials, so we need to use the
|
# We need a shell to provide the registry credentials, so we need to use the
|
||||||
# kaniko debug image (https://github.com/GoogleContainerTools/kaniko#debug-image)
|
# kaniko debug image (https://github.com/GoogleContainerTools/kaniko#debug-image)
|
||||||
name: gcr.io/kaniko-project/executor:debug
|
name: gcr.io/kaniko-project/executor:debug
|
||||||
entrypoint: [""]
|
entrypoint: [""]
|
||||||
script:
|
variables:
|
||||||
- cp deployment/Dockerfile web-build
|
KANIKO_BUILD_IMAGENAME: dashboard
|
||||||
- cp deployment/nginx.conf web-build
|
DIRECTORY: web-build
|
||||||
- cd web-build
|
before_script:
|
||||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
|
- cp deployment/Dockerfile $DIRECTORY
|
||||||
- /kaniko/executor --cache=true --context ${CI_PROJECT_DIR}/web-build --destination ${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_COMMIT_REF_SLUG}
|
- cp deployment/nginx.conf $DIRECTORY
|
||||||
|
extends:
|
||||||
|
.kaniko-build
|
||||||
|
|
||||||
|
build-backend-container:
|
||||||
|
stage: build-container
|
||||||
|
variables:
|
||||||
|
KANIKO_BUILD_IMAGENAME: dashboard-backend
|
||||||
|
DIRECTORY: backend
|
||||||
|
image:
|
||||||
|
# We need a shell to provide the registry credentials, so we need to use the
|
||||||
|
# kaniko debug image (https://github.com/GoogleContainerTools/kaniko#debug-image)
|
||||||
|
name: gcr.io/kaniko-project/executor:debug
|
||||||
|
entrypoint: [""]
|
||||||
|
extends:
|
||||||
|
.kaniko-build
|
||||||
|
|
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Fix login welcome message
|
||||||
|
- Clarify "set new password" button (#94)
|
||||||
|
- Show error messages when login fails, for example when a wrong password was
|
||||||
|
entered (#96)
|
||||||
|
|
||||||
|
## [0.5.1]
|
||||||
|
|
||||||
|
- Fix bug of missing "Monitoring" app access when creating a new user.
|
||||||
|
- Add Velero to the list of installable apps, but hide it from the dashboard
|
||||||
|
|
||||||
|
## [0.5.0]
|
||||||
|
|
||||||
|
- Merge dashboard-backend repository into this repository, released as 0.5.0
|
110
Header.tsx
Normal file
110
Header.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Disclosure } from '@headlessui/react';
|
||||||
|
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useApps } from 'src/services/apps';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { UserModal } from '../UserModal';
|
||||||
|
|
||||||
|
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
|
||||||
|
{ name: 'Users', to: '/users', requiresAdmin: true },
|
||||||
|
{ name: 'Apps', to: '/apps', requiresAdmin: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
function classNames(...classes: any[]) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNavigationByDashboardRole(isAdmin: boolean) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation.filter((item) => !item.requiresAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface HeaderProps {}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = () => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const { apps, loadApps, appTableLoading } = useApps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||||
|
<div className="relative flex justify-between h-10">
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
{open ? (
|
||||||
|
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</Disclosure.Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||||
|
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="block lg:hidden" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||||
|
<img className="hidden lg:block" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||||
|
</Link>
|
||||||
|
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||||
|
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||||
|
{apps.map((app) => (
|
||||||
|
<Link
|
||||||
|
key={app.name}
|
||||||
|
to={app.slug}
|
||||||
|
className={clsx(
|
||||||
|
'border-primary-50 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium litbutton',
|
||||||
|
{
|
||||||
|
'border-primary-500 litbutton-active hover:border-gray-300 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||||
|
pathname.includes(app.slug),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{app.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Disclosure.Panel className="sm:hidden">
|
||||||
|
<div className="pt-2 pb-4 space-y-1">
|
||||||
|
{
|
||||||
|
apps.map((app) => (
|
||||||
|
<Link
|
||||||
|
key={app.name}
|
||||||
|
to={app.slug}
|
||||||
|
className={clsx(
|
||||||
|
'border-transparent litbutton block pl-3 pr-4 py-2 border-l-4 litbutton text-base font-medium',
|
||||||
|
{
|
||||||
|
'litbutton-active border-primary-400 block pl-3 pr-4 py-2': pathname.includes(app.slug),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{app.name}
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
|
@ -8,4 +8,7 @@ __pycache__
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
run_app.local.sh
|
run_app.local.sh
|
||||||
|
<<<<<<< HEAD
|
||||||
*.db
|
*.db
|
||||||
|
=======
|
||||||
|
>>>>>>> main
|
||||||
|
|
|
@ -15,11 +15,19 @@ ADD requirements.txt .
|
||||||
# pip install the local requirements.txt
|
# pip install the local requirements.txt
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# now copy all the files in this directory to /app
|
# now copy all the files in this directory to /app
|
||||||
|
=======
|
||||||
|
# now copy all the files in this directory to /code
|
||||||
|
>>>>>>> main
|
||||||
ADD . .
|
ADD . .
|
||||||
|
|
||||||
# Listen to port 80 at runtime
|
# Listen to port 80 at runtime
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
# Define our command to be run when launching the container
|
# Define our command to be run when launching the container
|
||||||
|
<<<<<<< HEAD
|
||||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||||
|
=======
|
||||||
|
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:5000", "--workers", "4", "--reload", "--capture-output", "--enable-stdio-inheritance", "--log-level", "DEBUG"]
|
||||||
|
>>>>>>> main
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
from .models_lit import LITApp
|
|
||||||
from .models import App, AppRole
|
from .models import App, AppRole
|
||||||
from database import db
|
|
||||||
|
|
||||||
class AppsService:
|
class AppsService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
def env_file(key: str):
|
def env_file(key: str):
|
||||||
file_env = os.environ.get(f"{key}_FILE")
|
file_env = os.environ.get(f"{key}_FILE")
|
||||||
if os.path.exists(file_env):
|
if file_env and os.path.exists(file_env):
|
||||||
return open(file_env).read().rstrip('\n')
|
return open(file_env).read().rstrip('\n')
|
||||||
return os.environ.get(key)
|
return os.environ.get(key)
|
||||||
|
|
||||||
|
@ -28,4 +28,4 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
# Set this to "true" to load the config from a Kubernetes serviceaccount
|
# Set this to "true" to load the config from a Kubernetes serviceaccount
|
||||||
# running in a Kubernetes pod. Set it to "false" to load the config from the
|
# running in a Kubernetes pod. Set it to "false" to load the config from the
|
||||||
# `KUBECONFIG` environment variable.
|
# `KUBECONFIG` environment variable.
|
||||||
LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG") == "true"
|
LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true"
|
||||||
|
|
46
backend/migrations/versions/27761560bbcb_.py
Normal file
46
backend/migrations/versions/27761560bbcb_.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 27761560bbcb
|
||||||
|
Revises:
|
||||||
|
Create Date: 2021-12-21 06:07:14.857940
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "27761560bbcb"
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"app",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=64), nullable=True),
|
||||||
|
sa.Column("slug", sa.String(length=64), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("slug"),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"app_role",
|
||||||
|
sa.Column("user_id", sa.String(length=64), nullable=False),
|
||||||
|
sa.Column("app_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("role", sa.String(length=64), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["app_id"],
|
||||||
|
["app.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("user_id", "app_id"),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("app_role")
|
||||||
|
op.drop_table("app")
|
||||||
|
# ### end Alembic commands ###
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""add-velero-as-app
|
||||||
|
|
||||||
|
Revision ID: 3fa0c38ea1ac
|
||||||
|
Revises: e08df0bef76f
|
||||||
|
Create Date: 2022-10-13 09:40:44.290319
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3fa0c38ea1ac'
|
||||||
|
down_revision = 'e08df0bef76f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Add monitoring app
|
||||||
|
op.execute(f'INSERT IGNORE INTO app (`name`, `slug`) VALUES ("Velero","velero")')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""convert role column to table
|
||||||
|
|
||||||
|
Revision ID: 5f462d2d9d25
|
||||||
|
Revises: 27761560bbcb
|
||||||
|
Create Date: 2022-04-13 15:00:27.182898
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "5f462d2d9d25"
|
||||||
|
down_revision = "27761560bbcb"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
role_table = op.create_table(
|
||||||
|
"role",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=64), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.add_column("app_role", sa.Column("role_id", sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, "app_role", "role", ["role_id"], ["id"])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
# Insert default role "admin" as ID 1
|
||||||
|
op.execute(sa.insert(role_table).values(id=1,name="admin"))
|
||||||
|
# Set role_id 1 to all current "admin" users
|
||||||
|
op.execute("UPDATE app_role SET role_id = 1 WHERE role = 'admin'")
|
||||||
|
|
||||||
|
# Drop old column
|
||||||
|
op.drop_column("app_role", "role")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
"app_role", sa.Column("role", mysql.VARCHAR(length=64), nullable=True)
|
||||||
|
)
|
||||||
|
op.drop_constraint(None, "app_role", type_="foreignkey")
|
||||||
|
op.drop_column("app_role", "role_id")
|
||||||
|
op.drop_table("role")
|
||||||
|
# ### end Alembic commands ###
|
76
backend/migrations/versions/b514cca2d47b_add_user_role.py
Normal file
76
backend/migrations/versions/b514cca2d47b_add_user_role.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""update apps and add 'user' and 'no access' role
|
||||||
|
|
||||||
|
Revision ID: b514cca2d47b
|
||||||
|
Revises: 5f462d2d9d25
|
||||||
|
Create Date: 2022-06-08 17:24:51.305129
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b514cca2d47b'
|
||||||
|
down_revision = '5f462d2d9d25'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
# Check and update app table in DB
|
||||||
|
apps = {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"wekan": "Wekan",
|
||||||
|
"wordpress": "WordPress",
|
||||||
|
"nextcloud": "Nextcloud",
|
||||||
|
"zulip": "Zulip"
|
||||||
|
}
|
||||||
|
# app table
|
||||||
|
app_table = sa.table('app', sa.column('id', sa.Integer), sa.column(
|
||||||
|
'name', sa.String), sa.column('slug', sa.String))
|
||||||
|
|
||||||
|
existing_apps = op.get_bind().execute(app_table.select()).fetchall()
|
||||||
|
existing_app_slugs = [app['slug'] for app in existing_apps]
|
||||||
|
for app_slug in apps.keys():
|
||||||
|
if app_slug in existing_app_slugs:
|
||||||
|
op.execute(f'UPDATE app SET `name` = "{apps.get(app_slug)}" WHERE slug = "{app_slug}"')
|
||||||
|
else:
|
||||||
|
op.execute(f'INSERT INTO app (`name`, slug) VALUES ("{apps.get(app_slug)}","{app_slug}")')
|
||||||
|
|
||||||
|
# Fetch all apps including newly created
|
||||||
|
existing_apps = op.get_bind().execute(app_table.select()).fetchall()
|
||||||
|
# Insert role "user" as ID 2
|
||||||
|
op.execute("INSERT INTO `role` (id, `name`) VALUES (2, 'user')")
|
||||||
|
# Insert role "no access" as ID 3
|
||||||
|
op.execute("INSERT INTO `role` (id, `name`) VALUES (3, 'no access')")
|
||||||
|
# Set role_id 2 to all current "user" users which by have NULL role ID
|
||||||
|
op.execute("UPDATE app_role SET role_id = 2 WHERE role_id IS NULL")
|
||||||
|
|
||||||
|
# Add 'no access' role for all users that don't have any roles for specific apps
|
||||||
|
app_roles_table = sa.table('app_role', sa.column('user_id', sa.String), sa.column(
|
||||||
|
'app_id', sa.Integer), sa.column('role_id', sa.Integer))
|
||||||
|
|
||||||
|
app_ids = [app['id'] for app in existing_apps]
|
||||||
|
app_roles = op.get_bind().execute(app_roles_table.select()).fetchall()
|
||||||
|
user_ids = set([app_role['user_id'] for app_role in app_roles])
|
||||||
|
|
||||||
|
for user_id in user_ids:
|
||||||
|
existing_user_app_ids = [x['app_id'] for x in list(filter(lambda role: role['user_id'] == user_id, app_roles))]
|
||||||
|
missing_user_app_ids = [x for x in app_ids if x not in existing_user_app_ids]
|
||||||
|
|
||||||
|
if len(missing_user_app_ids) > 0:
|
||||||
|
values = [{'user_id': user_id, 'app_id': app_id, 'role_id': 3} for app_id in missing_user_app_ids]
|
||||||
|
op.bulk_insert(app_roles_table, values)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Revert all users role_id to NULL where role is 'user'
|
||||||
|
op.execute("UPDATE app_role SET role_id = NULL WHERE role_id = 2")
|
||||||
|
# Delete role 'user' from roles
|
||||||
|
op.execute("DELETE FROM `role` WHERE id = 2")
|
||||||
|
|
||||||
|
# Delete all user app roles where role is 'no access' with role_id 3
|
||||||
|
op.execute("DELETE FROM app_role WHERE role_id = 3")
|
||||||
|
# Delete role 'no access' from roles
|
||||||
|
op.execute("DELETE FROM `role` WHERE id = 3")
|
33
backend/migrations/versions/e08df0bef76f_.py
Normal file
33
backend/migrations/versions/e08df0bef76f_.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""Add fields for external apps
|
||||||
|
|
||||||
|
Revision ID: e08df0bef76f
|
||||||
|
Revises: b514cca2d47b
|
||||||
|
Create Date: 2022-09-23 16:38:06.557307
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e08df0bef76f'
|
||||||
|
down_revision = 'b514cca2d47b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('app', sa.Column('external', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
op.add_column('app', sa.Column('url', sa.String(length=128), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
# Add monitoring app
|
||||||
|
op.execute(f'INSERT IGNORE INTO app (`name`, `slug`) VALUES ("Monitoring","monitoring")')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('app', 'url')
|
||||||
|
op.drop_column('app', 'external')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1,5 +1,25 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.5.1]
|
||||||
|
|
||||||
|
* Update dashboard to version 0.5.1
|
||||||
|
|
||||||
|
## [1.5.0]
|
||||||
|
|
||||||
|
* Use dashboard backend container from new location
|
||||||
|
* Use proper semver image tags
|
||||||
|
|
||||||
|
## [1.4.0]
|
||||||
|
|
||||||
|
* Fetch apps from Kubernetes back-end
|
||||||
|
* Also fetch external apps
|
||||||
|
* Only show installed apps on dashboard
|
||||||
|
* Use correct URLs for apps on dashboard (not hardcoded ones)
|
||||||
|
|
||||||
|
## [1.2.3]
|
||||||
|
|
||||||
|
* Fix logging out of Hydra from the dashboard
|
||||||
|
|
||||||
## [1.2.2]
|
## [1.2.2]
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: common
|
- name: common
|
||||||
repository: https://charts.bitnami.com/bitnami
|
repository: https://charts.bitnami.com/bitnami
|
||||||
version: 2.0.1
|
version: 2.0.3
|
||||||
digest: sha256:eac8729956b60d78414de3eea46b919b44afcd7afdcd19dacd640269b3d731f2
|
digest: sha256:dfd07906c97f7fca7593af69d01f6f044e10a609a03057352142766a5caca6cd
|
||||||
generated: "2022-08-24T15:52:13.18511608+02:00"
|
generated: "2022-09-29T15:38:57.444746866+02:00"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
annotations:
|
annotations:
|
||||||
category: Dashboard
|
category: Dashboard
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.2.8
|
appVersion: 0.5.1
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: common
|
- name: common
|
||||||
# https://artifacthub.io/packages/helm/bitnami/common
|
# https://artifacthub.io/packages/helm/bitnami/common
|
||||||
|
@ -23,4 +23,4 @@ name: stackspin-dashboard
|
||||||
sources:
|
sources:
|
||||||
- https://open.greenhost.net/stackspin/dashboard/
|
- https://open.greenhost.net/stackspin/dashboard/
|
||||||
- https://open.greenhost.net/stackspin/dashboard-backend/
|
- https://open.greenhost.net/stackspin/dashboard-backend/
|
||||||
version: 1.2.2
|
version: 1.5.1
|
||||||
|
|
|
@ -24,6 +24,7 @@ data:
|
||||||
HYDRA_ADMIN_URL: {{ .Values.backend.hydra.adminUrl }}
|
HYDRA_ADMIN_URL: {{ .Values.backend.hydra.adminUrl }}
|
||||||
LOGIN_PANEL_URL: {{ .Values.backend.loginPanelUrl }}
|
LOGIN_PANEL_URL: {{ .Values.backend.loginPanelUrl }}
|
||||||
DATABASE_URL: {{ .Values.backend.databaseUrl }}
|
DATABASE_URL: {{ .Values.backend.databaseUrl }}
|
||||||
|
LOAD_INCLUSTER_CONFIG: "true"
|
||||||
# {{- if .Values.backend.smtp.enabled }}
|
# {{- if .Values.backend.smtp.enabled }}
|
||||||
# DASHBOARD_BACKEND_SMTP_HOST: {{ .Values.backend.smtp.host | quote }}
|
# DASHBOARD_BACKEND_SMTP_HOST: {{ .Values.backend.smtp.host | quote }}
|
||||||
# DASHBOARD_BACKEND_SMTP_PORT: {{ .Values.backend.smtp.port | quote }}
|
# DASHBOARD_BACKEND_SMTP_PORT: {{ .Values.backend.smtp.port | quote }}
|
||||||
|
|
46
deployment/helmchart/templates/rbac/clusterrole.yaml
Normal file
46
deployment/helmchart/templates/rbac/clusterrole.yaml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{{- if .Values.rbac.create -}}
|
||||||
|
apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }}
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: {{ template "common.names.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace | quote }}
|
||||||
|
labels: {{- include "common.labels.standard" . | nindent 4 }}
|
||||||
|
{{- if .Values.commonLabels }}
|
||||||
|
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.commonAnnotations }}
|
||||||
|
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- kustomize.toolkit.fluxcd.io
|
||||||
|
resources:
|
||||||
|
- kustomizations
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- helm.toolkit.fluxcd.io
|
||||||
|
resources:
|
||||||
|
- helmreleases
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- secrets
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- create
|
||||||
|
{{- end }}
|
22
deployment/helmchart/templates/rbac/clusterrolebinding.yaml
Normal file
22
deployment/helmchart/templates/rbac/clusterrolebinding.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{{- if .Values.rbac.create -}}
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }}
|
||||||
|
metadata:
|
||||||
|
name: {{ template "common.names.fullname" . }}
|
||||||
|
labels: {{- include "common.labels.standard" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: server
|
||||||
|
{{- if .Values.commonLabels }}
|
||||||
|
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.commonAnnotations }}
|
||||||
|
annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: {{ template "common.names.fullname" . }}
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: {{ include "dashboard.serviceAccountName" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
{{- end }}
|
22
deployment/helmchart/templates/rbac/serviceaccount.yaml
Normal file
22
deployment/helmchart/templates/rbac/serviceaccount.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{{- if and .Values.rbac.create .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "dashboard.serviceAccountName" . }}
|
||||||
|
namespace: {{ .Release.Namespace | quote }}
|
||||||
|
labels: {{- include "common.labels.standard" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: server
|
||||||
|
{{- if .Values.commonLabels }}
|
||||||
|
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or .Values.serviceAccount.annotations .Values.commonAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- if .Values.commonAnnotations }}
|
||||||
|
{{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.serviceAccount.annotations }}
|
||||||
|
{{- include "common.tplvalues.render" (dict "value" .Values.serviceAccount.annotations "context" $) | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
|
||||||
|
{{- end }}
|
|
@ -1,19 +0,0 @@
|
||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ include "dashboard.serviceAccountName" . }}
|
|
||||||
labels: {{- include "common.labels.standard" . | nindent 4 }}
|
|
||||||
{{- if .Values.commonLabels }}
|
|
||||||
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if or .Values.serviceAccount.annotations .Values.commonAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- if .Values.serviceAccount.annotations }}
|
|
||||||
{{- toYaml .Values.serviceAccount.annotations | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if .Values.commonAnnotations }}
|
|
||||||
{{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end -}}
|
|
|
@ -68,7 +68,7 @@ dashboard:
|
||||||
image:
|
image:
|
||||||
registry: open.greenhost.net:4567
|
registry: open.greenhost.net:4567
|
||||||
repository: stackspin/dashboard/dashboard
|
repository: stackspin/dashboard/dashboard
|
||||||
tag: 0-2-8
|
tag: 0.5.1
|
||||||
digest: ""
|
digest: ""
|
||||||
## Optionally specify an array of imagePullSecrets.
|
## Optionally specify an array of imagePullSecrets.
|
||||||
## Secrets must be manually created in the namespace.
|
## Secrets must be manually created in the namespace.
|
||||||
|
@ -235,8 +235,8 @@ backend:
|
||||||
##
|
##
|
||||||
image:
|
image:
|
||||||
registry: open.greenhost.net:4567
|
registry: open.greenhost.net:4567
|
||||||
repository: stackspin/dashboard-backend/dashboard-backend
|
repository: stackspin/dashboard/dashboard-backend
|
||||||
tag: 0-2-9
|
tag: 0.5.1
|
||||||
digest: ""
|
digest: ""
|
||||||
## Optionally specify an array of imagePullSecrets.
|
## Optionally specify an array of imagePullSecrets.
|
||||||
## Secrets must be manually created in the namespace.
|
## Secrets must be manually created in the namespace.
|
||||||
|
@ -695,3 +695,26 @@ ingress:
|
||||||
## key:
|
## key:
|
||||||
## certificate:
|
## certificate:
|
||||||
secrets: []
|
secrets: []
|
||||||
|
|
||||||
|
# The dashboard-backend needs access to certain Kubernetes APIs to be able to
|
||||||
|
# install and remove apps
|
||||||
|
rbac:
|
||||||
|
## @param backend.rbac.create Specifies whether RBAC resources should be created
|
||||||
|
create: true
|
||||||
|
|
||||||
|
## ServiceAccount configuration for dashboard backend
|
||||||
|
##
|
||||||
|
serviceAccount:
|
||||||
|
## @param backend.serviceAccount.create Specifies whether a ServiceAccount should be created
|
||||||
|
##
|
||||||
|
create: true
|
||||||
|
## @param backend.serviceAccount.name The name of the ServiceAccount to use.
|
||||||
|
## If not set and create is true, a name is generated using the common.names.fullname template
|
||||||
|
##
|
||||||
|
name: ""
|
||||||
|
## @param backend.serviceAccount.automountServiceAccountToken Automount service account token for the dashboard backend service account
|
||||||
|
##
|
||||||
|
automountServiceAccountToken: true
|
||||||
|
## @param backend.serviceAccount.annotations Annotations for service account. Evaluated as a template. Only used if `create` is `true`.
|
||||||
|
##
|
||||||
|
annotations: {}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
|
57
public/assets/monitoring.svg
Normal file
57
public/assets/monitoring.svg
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:url(#SVGID_1_);}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-0.7593" y1="513.0059" x2="1.0811" y2="513.0059" gradientTransform="matrix(1.431349e-14 -254.0268 254.0268 1.690345e-14 -130061.25 434.0558)">
|
||||||
|
<stop offset="0" style="stop-color:#FFF100"/>
|
||||||
|
<stop offset="1" style="stop-color:#F05A28"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path class="st0" d="M490.8,226c-0.8-8.6-2.3-18.5-5.1-29.5c-2.8-10.9-7.1-22.8-13.3-35.3c-6.2-12.4-14.2-25.2-24.7-37.8
|
||||||
|
c-4.1-4.9-8.6-9.7-13.4-14.4c7.2-28.6-8.7-53.5-8.7-53.5c-27.5-1.7-45,8.6-51.5,13.3c-1.1-0.4-2.1-1-3.2-1.4
|
||||||
|
c-4.7-1.8-9.5-3.7-14.5-5.2c-4.9-1.6-10-3-15.2-4.2c-5.2-1.3-10.4-2.3-15.8-3.1c-1-0.1-1.8-0.3-2.8-0.4C310.6,16.1,276.1,0,276.1,0
|
||||||
|
c-38.5,24.4-45.7,58.5-45.7,58.5s-0.1,0.7-0.4,2c-2.1,0.6-4.2,1.3-6.3,1.8c-3,0.8-5.9,2-8.7,3.1c-3,1.1-5.8,2.3-8.7,3.5
|
||||||
|
c-5.8,2.5-11.6,5.4-17.2,8.5c-5.5,3.1-10.9,6.5-16.1,10c-0.7-0.3-1.4-0.6-1.4-0.6C118.2,66.6,70.9,91,70.9,91
|
||||||
|
c-4.4,56.7,21.3,92.4,26.4,98.9c-1.3,3.5-2.4,7.1-3.5,10.6c-3.9,12.8-6.9,26-8.7,39.6c-0.3,2-0.6,3.9-0.7,5.9
|
||||||
|
c-49.4,24.4-63.9,74.2-63.9,74.2c41,47.3,89,50.2,89,50.2l0.1-0.1c6.1,10.9,13.1,21.2,21,30.9c3.4,4.1,6.8,7.9,10.4,11.7
|
||||||
|
c-15,42.9,2.1,78.4,2.1,78.4c45.7,1.7,75.7-20,82.1-25c4.5,1.6,9.2,3,13.8,4.1c14.1,3.7,28.5,5.8,42.9,6.3
|
||||||
|
c3.5,0.1,7.2,0.3,10.7,0.1h1.7h1.1h2.3l2.3-0.1v0.1c21.6,30.7,59.4,35.1,59.4,35.1c26.9-28.4,28.5-56.6,28.5-62.6c0,0,0-0.1,0-0.4
|
||||||
|
c0-0.6,0-0.8,0-0.8c0-0.4,0-0.8,0-1.3c5.6-4,11-8.2,16.1-12.8c10.7-9.7,20.2-20.9,28.1-32.9c0.7-1.1,1.4-2.3,2.1-3.4
|
||||||
|
c30.5,1.7,52-18.9,52-18.9c-5.1-31.7-23.1-47.3-26.9-50.2c0,0-0.1-0.1-0.4-0.3c-0.3-0.1-0.3-0.3-0.3-0.3c-0.1-0.1-0.4-0.3-0.7-0.4
|
||||||
|
c0.1-2,0.3-3.8,0.4-5.8c0.3-3.4,0.3-6.9,0.3-10.3V309v-1.3v-0.7c0-0.8,0-0.6,0-0.8l-0.1-2.1l-0.1-2.8c0-1-0.1-1.8-0.3-2.7
|
||||||
|
c-0.1-0.8-0.1-1.8-0.3-2.7l-0.3-2.7l-0.4-2.7c-0.6-3.5-1.1-6.9-2-10.4c-3.2-13.7-8.6-26.7-15.5-38.4c-7.1-11.7-15.8-22-25.8-30.7
|
||||||
|
c-9.9-8.7-21-15.8-32.6-21c-11.7-5.2-23.8-8.6-36-10.2c-6.1-0.8-12.1-1.1-18.2-1h-2.3h-0.6h-0.7h-1l-2.3,0.1
|
||||||
|
c-0.8,0-1.7,0.1-2.4,0.1c-3.1,0.3-6.2,0.7-9.2,1.3c-12.1,2.3-23.6,6.6-33.6,12.7c-10,6.1-18.8,13.5-25.8,22
|
||||||
|
c-7.1,8.5-12.6,17.9-16.4,27.6s-5.9,19.9-6.5,29.6c-0.1,2.4-0.1,4.9-0.1,7.3c0,0.6,0,1.3,0,1.8l0.1,2c0.1,1.1,0.1,2.4,0.3,3.5
|
||||||
|
c0.4,4.9,1.4,9.7,2.7,14.2c2.7,9.2,6.9,17.5,12.1,24.5c5.2,7.1,11.6,12.8,18.2,17.5c6.6,4.5,13.8,7.8,20.9,9.9
|
||||||
|
c7.1,2.1,14.1,3,20.7,3c0.8,0,1.7,0,2.4,0c0.4,0,0.8,0,1.3,0s0.8,0,1.3-0.1c0.7,0,1.4-0.1,2.1-0.1l0.6-0.1l0.7-0.1
|
||||||
|
c0.4,0,0.8-0.1,1.3-0.1c0.8-0.1,1.6-0.3,2.4-0.4c0.8-0.1,1.6-0.3,2.3-0.6c1.6-0.3,3-0.8,4.4-1.3c2.8-1,5.6-2.1,8-3.4
|
||||||
|
c2.5-1.3,4.8-2.8,7.1-4.2c0.6-0.4,1.3-0.8,1.8-1.4c2.3-1.8,2.7-5.2,0.8-7.5c-1.6-2-4.4-2.5-6.6-1.3c-0.6,0.3-1.1,0.6-1.7,0.8
|
||||||
|
c-2,1-3.9,1.8-6.1,2.5c-2.1,0.7-4.4,1.3-6.6,1.7c-1.1,0.1-2.3,0.3-3.5,0.4c-0.6,0-1.1,0.1-1.8,0.1c-0.6,0-1.3,0-1.7,0
|
||||||
|
c-0.6,0-1.1,0-1.7,0c-0.7,0-1.4,0-2.1-0.1c0,0-0.4,0-0.1,0h-0.3h-0.4c-0.3,0-0.7,0-1-0.1c-0.7-0.1-1.3-0.1-2-0.3
|
||||||
|
c-5.2-0.7-10.4-2.3-15.4-4.5c-5.1-2.3-9.9-5.4-14.2-9.3c-4.4-4-8.2-8.6-11.1-14c-3-5.4-5.1-11.3-6.1-17.5c-0.4-3.1-0.7-6.3-0.6-9.5
|
||||||
|
c0-0.8,0.1-1.7,0.1-2.5c0,0.3,0-0.1,0-0.1v-0.3v-0.7c0-0.4,0.1-0.8,0.1-1.3c0.1-1.7,0.4-3.4,0.7-5.1c2.4-13.5,9.2-26.8,19.6-36.8
|
||||||
|
c2.7-2.5,5.5-4.8,8.5-6.9c3-2.1,6.2-3.9,9.6-5.5c3.4-1.6,6.8-2.8,10.4-3.8c3.5-1,7.2-1.6,11-2c1.8-0.1,3.7-0.3,5.6-0.3
|
||||||
|
c0.6,0,0.8,0,1.3,0h1.6h1c0.4,0,0,0,0.1,0h0.4l1.6,0.1c4.1,0.3,8,0.8,12,1.8c7.9,1.7,15.7,4.7,22.8,8.6c14.4,8,26.7,20.5,34.1,35.4
|
||||||
|
c3.8,7.5,6.5,15.5,7.8,23.8c0.3,2.1,0.6,4.2,0.7,6.3l0.1,1.6l0.1,1.6c0,0.6,0,1.1,0,1.6c0,0.6,0,1.1,0,1.6v1.4v1.6
|
||||||
|
c0,1-0.1,2.7-0.1,3.7c-0.1,2.3-0.4,4.7-0.7,6.9c-0.3,2.3-0.7,4.5-1.1,6.8c-0.4,2.3-1,4.5-1.6,6.6c-1.1,4.4-2.5,8.7-4.2,13.1
|
||||||
|
c-3.4,8.5-7.9,16.6-13.3,24.1c-10.9,15-25.7,27.1-42.6,34.8c-8.5,3.8-17.3,6.6-26.5,8c-4.5,0.8-9.2,1.3-13.8,1.4h-0.8h-0.7h-1.6
|
||||||
|
h-2.3h-1.1c0.6,0-0.1,0-0.1,0h-0.4c-2.5,0-4.9-0.1-7.5-0.4c-9.9-0.7-19.6-2.5-29.2-5.2c-9.5-2.7-18.6-6.5-27.4-11
|
||||||
|
c-17.3-9.3-33-22-45.1-37.4c-6.1-7.6-11.4-15.9-15.8-24.5c-4.4-8.6-7.9-17.8-10.4-26.9c-2.5-9.3-4.1-18.8-4.8-28.4l-0.1-1.8v-0.4
|
||||||
|
v-0.4v-0.8v-1.6v-0.4v-0.6v-1.1V268v-0.4c0,0,0,0.1,0-0.1v-0.8c0-1.1,0-2.4,0-3.5c0.1-4.7,0.6-9.6,1.1-14.4
|
||||||
|
c0.6-4.8,1.4-9.7,2.4-14.5c1-4.8,2.1-9.6,3.5-14.4c2.7-9.5,6.1-18.6,10-27.2c8-17.2,18.5-32.6,31-44.9c3.1-3.1,6.3-5.9,9.7-8.7
|
||||||
|
c3.4-2.7,6.9-5.2,10.6-7.6c3.5-2.4,7.3-4.5,11.1-6.5c1.8-1,3.8-2,5.8-2.8c1-0.4,2-0.8,3-1.3c1-0.4,2-0.8,3-1.3
|
||||||
|
c3.9-1.7,8-3.1,12.3-4.4c1-0.3,2.1-0.6,3.1-1c1-0.3,2.1-0.6,3.1-0.8c2.1-0.6,4.2-1.1,6.3-1.6c1-0.3,2.1-0.4,3.2-0.7
|
||||||
|
c1.1-0.3,2.1-0.4,3.2-0.7c1.1-0.1,2.1-0.4,3.2-0.6l1.6-0.3l1.7-0.3c1.1-0.1,2.1-0.3,3.2-0.4c1.3-0.1,2.4-0.3,3.7-0.4
|
||||||
|
c1-0.1,2.7-0.3,3.7-0.4c0.7-0.1,1.6-0.1,2.3-0.3l1.6-0.1l0.7-0.1h0.8c1.3-0.1,2.4-0.1,3.7-0.3l1.8-0.1c0,0,0.7,0,0.1,0h0.4h0.8
|
||||||
|
c1,0,2.1-0.1,3.1-0.1c4.1-0.1,8.3-0.1,12.4,0c8.2,0.3,16.2,1.3,24,2.7c15.7,3,30.3,7.9,43.7,14.5c13.4,6.5,25.2,14.5,35.7,23.3
|
||||||
|
c0.7,0.6,1.3,1.1,2,1.7c0.6,0.6,1.3,1.1,1.8,1.7c1.3,1.1,2.4,2.3,3.7,3.4c1.3,1.1,2.4,2.3,3.5,3.4c1.1,1.1,2.3,2.3,3.4,3.5
|
||||||
|
c4.4,4.7,8.5,9.3,12.1,14.1c7.3,9.5,13.3,19,17.9,28.1c0.3,0.6,0.6,1.1,0.8,1.7s0.6,1.1,0.8,1.7c0.6,1.1,1.1,2.3,1.6,3.4
|
||||||
|
c0.6,1.1,1,2.1,1.6,3.2c0.4,1.1,1,2.1,1.4,3.2c1.7,4.2,3.4,8.3,4.7,12.1c2.1,6.2,3.7,11.7,4.9,16.5c0.4,2,2.3,3.2,4.2,3
|
||||||
|
c2.1-0.1,3.7-1.8,3.7-3.9C491.7,238.9,491.5,232.9,490.8,226z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
21
public/assets/velero.svg
Normal file
21
public/assets/velero.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
|
||||||
|
<svg width="6cm" height="6cm" viewBox="6 5 106 106" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs/>
|
||||||
|
<g id="Background">
|
||||||
|
<g>
|
||||||
|
<path style="fill: none; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" d="M 112.1 60.8 C 112.1,60.1 112.2,59.4 112.2,58.8 C 112.2,58.1 112.2,57.4 112.1,56.8 C 112.1,58.1333 112.1,59.4667 112.1,60.8"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 96.9 92.9 C 93.6,92.9 92,94.1 90.2,95.6 C 88.2,97.2 86,99 81.6,99 C 77.3,99 75,97.2 73,95.6 C 71.2,94.2 69.6,92.9 66.3,92.9 C 63,92.9 61.4,94.1 59.6,95.6 C 57.6,97.2 55.4,99 51,99 C 46.7,99 44.4,97.2 42.4,95.6 C 40.6,94.2 39,92.9 35.7,92.9 C 32.4,92.9 30.8,94.1 29,95.6 C 27.7,96.6 26.3,97.8 24.2,98.4 C 32.5,105.7 43.1,110.4 54.7,111.4 C 55,111.4 55.3,111.5 55.7,111.5 C 56.8,111.6 58,111.6 59.1,111.6 C 60.3,111.6 61.4,111.5 62.5,111.5 C 62.8,111.5 63.1,111.5 63.5,111.4 C 77.7,110.2 90.3,103.4 99.1,93.2 C 98.6,93.1 97.8,92.9 96.9,92.9z"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 20.7 53.5 C 24.3,53.5 26,52.1 27.9,50.6 C 29.9,49 31.9,47.4 35.9,47.4 C 39.9,47.4 42,49 43.9,50.6 C 45.8,52.1 47.5,53.5 51.1,53.5 C 54.7,53.5 56.4,52.1 58.3,50.6 C 60.3,49 62.3,47.4 66.3,47.4 C 70.3,47.4 72.4,49 74.3,50.6 C 76.2,52.1 77.9,53.5 81.5,53.5 C 85.1,53.5 86.8,52.1 88.7,50.6 C 90.7,49 92.7,47.4 96.7,47.4 C 100.7,47.4 102.8,49 104.7,50.6 C 106.5,52.1 108.3,53.4 111.7,53.5 C 111.2,48.7 110.1,44 108.4,39.7 C 106.6,39.1 105.3,38.1 104,37.1 C 102.1,35.6 100.3,34.2 96.6,34.2 C 92.9,34.2 91.1,35.6 89.2,37.1 C 87.3,38.6 85.3,40.2 81.3,40.2 C 77.3,40.2 75.4,38.6 73.4,37.1 C 71.5,35.6 69.7,34.2 66,34.2 C 62.3,34.2 60.5,35.6 58.6,37.1 C 56.7,38.6 54.7,40.2 50.7,40.2 C 46.7,40.2 44.8,38.6 42.8,37.1 C 40.9,35.6 39.1,34.2 35.4,34.2 C 31.7,34.2 29.9,35.6 28,37.1 C 26.1,38.6 24.1,40.2 20.1,40.2 C 16.1,40.2 14.2,38.6 12.2,37.1 C 11.8,36.8 11.3,36.4 10.9,36.1 C 9.2,39.7 7.8,43.6 7,47.7 C 9.7,48.2 11.3,49.4 12.9,50.7 C 15.4,52.1 17.1,53.5 20.7,53.5z"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 20.7 39.3 C 24.4,39.3 26.2,37.9 28.1,36.4 C 30,34.9 32,33.3 36,33.3 C 40,33.3 41.9,34.9 43.9,36.4 C 45.8,37.9 47.6,39.3 51.3,39.3 C 55,39.3 56.8,37.9 58.7,36.4 C 60.6,34.9 62.6,33.3 66.6,33.3 C 70.6,33.3 72.5,34.9 74.5,36.4 C 76.4,37.9 78.2,39.3 81.9,39.3 C 85.6,39.3 87.4,37.9 89.3,36.4 C 91.2,34.9 93.2,33.3 97.2,33.3 C 101.2,33.3 103.1,34.9 105.1,36.4 C 106.2,37.2 107.2,38.1 108.5,38.6 C 100.6,19.4 81.6,5.8 59.6,5.8 C 38.8,5.8 20.9,17.8 12.2,35.3 C 12.7,35.7 13.2,36 13.6,36.4 C 15.2,37.9 17,39.3 20.7,39.3z"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 96.9 78.2 C 93.5,78.2 91.9,79.5 90.1,81 C 88.2,82.5 86,84.3 81.7,84.3 C 77.5,84.3 75.3,82.6 73.3,81 C 71.4,79.5 69.8,78.2 66.5,78.2 C 63.1,78.2 61.5,79.5 59.7,81 C 57.8,82.5 55.6,84.3 51.3,84.3 C 47.1,84.3 44.9,82.6 42.9,81 C 41,79.5 39.4,78.2 36.1,78.2 C 32.7,78.2 31.1,79.5 29.3,81 C 27.4,82.5 25.2,84.3 20.9,84.3 C 16.7,84.3 14.5,82.6 12.5,81 C 12,80.6 11.5,80.2 11,79.9 C 13.6,85.9 17.3,91.3 21.9,95.9 C 24.5,95.7 25.9,94.5 27.5,93.3 C 29.5,91.7 31.7,89.9 36.1,89.9 C 40.4,89.9 42.7,91.7 44.7,93.3 C 46.5,94.7 48.1,96 51.4,96 C 54.7,96 56.3,94.8 58.1,93.3 C 60.1,91.7 62.3,89.9 66.7,89.9 C 71,89.9 73.3,91.7 75.3,93.3 C 77.1,94.7 78.7,96 82,96 C 85.3,96 86.9,94.8 88.7,93.3 C 90.7,91.7 92.9,89.9 97.3,89.9 C 99.1,89.9 100.6,90.2 101.8,90.7 C 103.7,88.2 105.3,85.6 106.8,82.8 C 105.8,82.2 105,81.6 104.2,81 C 101.9,79.5 100.2,78.2 96.9,78.2z"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 112 54.9 C 108,54.9 106,53.3 104,51.7 C 102.1,50.2 100.4,48.8 96.8,48.8 C 93.2,48.8 91.5,50.2 89.6,51.7 C 87.6,53.3 85.6,54.9 81.6,54.9 C 77.6,54.9 75.5,53.3 73.6,51.7 C 71.7,50.2 70,48.8 66.4,48.8 C 62.8,48.8 61.1,50.2 59.2,51.7 C 57.2,53.3 55.2,54.9 51.2,54.9 C 47.2,54.9 45.1,53.3 43.2,51.7 C 41.3,50.2 39.6,48.8 36,48.8 C 32.4,48.8 30.7,50.2 28.8,51.7 C 26.8,53.3 24.8,54.9 20.8,54.9 C 16.8,54.9 14.7,53.3 12.8,51.7 C 11.3,50.5 9.8,49.4 7.5,49 C 7,51.5 6.7,54.1 6.6,56.7 C 6.6,57.4 6.5,58.1 6.5,58.7 C 6.5,59.4 6.5,60.1 6.6,60.7 C 6.6,61 6.6,61.3 6.6,61.6 C 10.1,61.9 12,63.4 13.8,64.8 C 15.6,66.2 17.3,67.6 20.8,67.6 C 24.3,67.6 26,66.2 27.8,64.8 C 29.7,63.3 31.9,61.6 36,61.6 C 40.1,61.6 42.3,63.3 44.2,64.8 C 46,66.2 47.7,67.6 51.2,67.6 C 54.7,67.6 56.4,66.2 58.2,64.8 C 60.1,63.3 62.3,61.6 66.4,61.6 C 70.5,61.6 72.7,63.3 74.6,64.8 C 76.4,66.2 78.1,67.6 81.6,67.6 C 85.1,67.6 86.8,66.2 88.6,64.8 C 90.5,63.3 92.7,61.6 96.8,61.6 C 100.9,61.6 103.1,63.3 105,64.8 C 106.7,66.1 108.3,67.4 111.3,67.6 C 111.6,66 111.8,64.4 111.9,62.7 C 111.9,62.1 112,61.4 112,60.8 C 112,59.4333 112,58.0667 112,56.7 C 112.1,56 112,54.9 112,54.9z"/>
|
||||||
|
<path style="fill: #009bdb; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 96.9 63.5 C 93.4,63.5 91.7,64.9 89.9,66.3 C 88,67.8 85.8,69.5 81.7,69.5 C 77.6,69.5 75.4,67.8 73.5,66.3 C 71.7,64.9 70,63.5 66.5,63.5 C 63,63.5 61.3,64.9 59.5,66.3 C 57.6,67.8 55.4,69.5 51.3,69.5 C 47.2,69.5 45,67.8 43.1,66.3 C 41.3,64.9 39.6,63.5 36.1,63.5 C 32.6,63.5 30.9,64.9 29.1,66.3 C 27.2,67.8 25,69.5 20.9,69.5 C 16.8,69.5 14.6,67.8 12.7,66.3 C 11.1,65 9.6,63.8 6.9,63.5 C 7.3,68 8.3,72.2 9.7,76.3 C 11.6,76.9 12.9,78 14.1,79 C 16,80.5 17.6,81.8 20.9,81.8 C 24.3,81.8 25.9,80.5 27.7,79 C 29.6,77.5 31.8,75.7 36.1,75.7 C 40.3,75.7 42.5,77.4 44.5,79 C 46.4,80.5 48,81.8 51.3,81.8 C 54.7,81.8 56.3,80.5 58.1,79 C 60,77.5 62.2,75.7 66.5,75.7 C 70.7,75.7 72.9,77.4 74.9,79 C 76.8,80.5 78.4,81.8 81.7,81.8 C 85.1,81.8 86.7,80.5 88.5,79 C 90.4,77.5 92.6,75.7 96.9,75.7 C 101.1,75.7 103.3,77.4 105.3,79 C 106,79.6 106.7,80.1 107.5,80.6 C 109.1,77.1 110.3,73.4 111.1,69.5 C 107.6,69.2 105.7,67.7 103.9,66.3 C 102.1,64.9 100.3,63.5 96.9,63.5z"/>
|
||||||
|
<path style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 88.3 93.3 C 86.5,94.7 84.9,96 81.6,96 C 78.3,96 76.7,94.8 74.9,93.3 C 72.9,91.7 70.7,89.9 66.3,89.9 C 62,89.9 59.7,91.7 57.7,93.3 C 55.9,94.7 54.3,96 51,96 C 47.7,96 46.1,94.8 44.3,93.3 C 42.3,91.7 40.1,89.9 35.7,89.9 C 31.4,89.9 29.1,91.7 27.1,93.3 C 25.5,94.6 24.1,95.7 21.5,95.9 C 22.4,96.8 23.3,97.6 24.2,98.4 C 26.3,97.8 27.7,96.6 29,95.6 C 30.8,94.2 32.4,92.9 35.7,92.9 C 39,92.9 40.6,94.1 42.4,95.6 C 44.4,97.2 46.6,99 51,99 C 55.3,99 57.6,97.2 59.6,95.6 C 61.4,94.2 63,92.9 66.3,92.9 C 69.6,92.9 71.2,94.1 73,95.6 C 75,97.2 77.2,99 81.6,99 C 85.9,99 88.2,97.2 90.2,95.6 C 92,94.2 93.6,92.9 96.9,92.9 C 97.8,92.9 98.6,93 99.3,93.2 C 100,92.4 100.7,91.5 101.3,90.7 C 100.1,90.2 98.6,89.9 96.8,89.9 C 92.5,89.9 90.3,91.7 88.3,93.3z"/>
|
||||||
|
<path style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 96.9 75.8 C 92.7,75.8 90.5,77.5 88.5,79.1 C 86.6,80.6 85,81.9 81.7,81.9 C 78.3,81.9 76.7,80.6 74.9,79.1 C 73,77.6 70.8,75.8 66.5,75.8 C 62.3,75.8 60.1,77.5 58.1,79.1 C 56.2,80.6 54.6,81.9 51.3,81.9 C 47.9,81.9 46.3,80.6 44.5,79.1 C 42.6,77.6 40.4,75.8 36.1,75.8 C 31.9,75.8 29.7,77.5 27.7,79.1 C 25.8,80.6 24.2,81.9 20.9,81.9 C 17.5,81.9 15.9,80.6 14.1,79.1 C 12.9,78.1 11.6,77.1 9.7,76.4 C 10.1,77.6 10.6,78.7 11.1,79.9 C 11.6,80.2 12.1,80.6 12.6,81 C 14.5,82.5 16.7,84.3 21,84.3 C 25.2,84.3 27.4,82.6 29.4,81 C 31.3,79.5 32.9,78.2 36.2,78.2 C 39.6,78.2 41.2,79.5 43,81 C 44.9,82.5 47.1,84.3 51.4,84.3 C 55.6,84.3 57.8,82.6 59.8,81 C 61.7,79.5 63.3,78.2 66.6,78.2 C 70,78.2 71.6,79.5 73.4,81 C 75.3,82.5 77.5,84.3 81.8,84.3 C 86,84.3 88.2,82.6 90.2,81 C 92.1,79.5 93.7,78.2 97,78.2 C 100.4,78.2 102,79.5 103.8,81 C 104.6,81.6 105.4,82.3 106.4,82.8 C 106.8,82.1 107.1,81.3 107.5,80.6 C 106.7,80.2 106.1,79.6 105.3,79 C 103.3,77.5 101.1,75.8 96.9,75.8z"/>
|
||||||
|
<path style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 96.9 61.6 C 92.8,61.6 90.6,63.3 88.7,64.8 C 86.9,66.2 85.2,67.6 81.7,67.6 C 78.2,67.6 76.5,66.2 74.7,64.8 C 72.8,63.3 70.6,61.6 66.5,61.6 C 62.4,61.6 60.2,63.3 58.3,64.8 C 56.5,66.2 54.8,67.6 51.3,67.6 C 47.8,67.6 46.1,66.2 44.3,64.8 C 42.4,63.3 40.2,61.6 36.1,61.6 C 32,61.6 29.8,63.3 27.9,64.8 C 26.1,66.2 24.4,67.6 20.9,67.6 C 17.4,67.6 15.7,66.2 13.9,64.8 C 12.2,63.4 10.2,61.9 6.7,61.6 C 6.7,62.2 6.8,62.9 6.8,63.5 C 9.5,63.8 11,65 12.6,66.3 C 14.5,67.8 16.7,69.5 20.8,69.5 C 24.9,69.5 27.1,67.8 29,66.3 C 30.8,64.9 32.5,63.5 36,63.5 C 39.5,63.5 41.2,64.9 43,66.3 C 44.9,67.8 47.1,69.5 51.2,69.5 C 55.3,69.5 57.5,67.8 59.4,66.3 C 61.2,64.9 62.9,63.5 66.4,63.5 C 69.9,63.5 71.6,64.9 73.4,66.3 C 75.3,67.8 77.5,69.5 81.6,69.5 C 85.7,69.5 87.9,67.8 89.8,66.3 C 91.6,64.9 93.3,63.5 96.8,63.5 C 100.3,63.5 102,64.9 103.8,66.3 C 105.5,67.7 107.5,69.2 111,69.5 C 111.1,68.9 111.2,68.2 111.3,67.6 C 108.3,67.4 106.7,66.2 105,64.8 C 103.2,63.3 101,61.6 96.9,61.6z"/>
|
||||||
|
<path style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 20.7 54.9 C 24.7,54.9 26.8,53.3 28.7,51.7 C 30.6,50.2 32.3,48.8 35.9,48.8 C 39.5,48.8 41.2,50.2 43.1,51.7 C 45.1,53.3 47.1,54.9 51.1,54.9 C 55.1,54.9 57.2,53.3 59.1,51.7 C 61,50.2 62.7,48.8 66.3,48.8 C 69.9,48.8 71.6,50.2 73.5,51.7 C 75.5,53.3 77.5,54.9 81.5,54.9 C 85.5,54.9 87.6,53.3 89.5,51.7 C 91.4,50.2 93.1,48.8 96.7,48.8 C 100.3,48.8 102,50.2 103.9,51.7 C 105.8,53.2 107.9,54.8 111.9,54.9 C 111.9,54.8667 111.9,54.8333 111.9,54.8 C 111.9,54.4 111.8,54 111.8,53.5 C 108.4,53.4 106.6,52.1 104.8,50.6 C 102.8,49 100.8,47.4 96.8,47.4 C 92.8,47.4 90.7,49 88.8,50.6 C 86.9,52.1 85.2,53.5 81.6,53.5 C 78,53.5 76.3,52.1 74.4,50.6 C 72.4,49 70.4,47.4 66.4,47.4 C 62.4,47.4 60.3,49 58.4,50.6 C 56.5,52.1 54.8,53.5 51.2,53.5 C 47.6,53.5 45.9,52.1 44,50.6 C 42,49 40,47.4 36,47.4 C 32,47.4 29.9,49 28,50.6 C 26.1,52.1 24.4,53.5 20.8,53.5 C 17.2,53.5 15.5,52.1 13.6,50.6 C 12,49.3 10.4,48.1 7.7,47.6 C 7.6,48 7.5,48.5 7.4,48.9 C 9.8,49.3 11.2,50.4 12.7,51.6 C 14.6,53.3 16.6,54.9 20.7,54.9z"/>
|
||||||
|
<path style="fill: #ffffff; fill-opacity: 1; stroke-opacity: 0; stroke-width: 0.02; stroke: #ffffff" fill-rule="evenodd" d="M 20.7 40.2 C 24.7,40.2 26.6,38.6 28.6,37.1 C 30.5,35.6 32.3,34.2 36,34.2 C 39.7,34.2 41.5,35.6 43.4,37.1 C 45.3,38.6 47.3,40.2 51.3,40.2 C 55.3,40.2 57.2,38.6 59.2,37.1 C 61.1,35.6 62.9,34.2 66.6,34.2 C 70.3,34.2 72.1,35.6 74,37.1 C 75.9,38.6 77.9,40.2 81.9,40.2 C 85.9,40.2 87.8,38.6 89.8,37.1 C 91.7,35.6 93.5,34.2 97.2,34.2 C 100.9,34.2 102.7,35.6 104.6,37.1 C 105.9,38.1 107.1,39.1 109,39.7 C 108.9,39.4 108.7,39 108.6,38.7 C 107.3,38.1 106.2,37.3 105.2,36.5 C 103.3,35 101.3,33.4 97.3,33.4 C 93.3,33.4 91.4,35 89.4,36.5 C 87.5,38 85.7,39.4 82,39.4 C 78.3,39.4 76.5,38 74.6,36.5 C 72.7,35 70.7,33.4 66.7,33.4 C 62.7,33.4 60.8,35 58.8,36.5 C 56.9,38 55.1,39.4 51.4,39.4 C 47.7,39.4 45.9,38 44,36.5 C 42.1,35 40.1,33.4 36.1,33.4 C 32.1,33.4 30.2,35 28.2,36.5 C 26.3,38 24.5,39.4 20.8,39.4 C 17.1,39.4 15.3,38 13.4,36.5 C 12.9,36.1 12.5,35.8 12,35.4 C 11.9,35.6 11.7,35.9 11.6,36.2 C 12,36.5 12.5,36.8 12.9,37.2 C 14.7,38.6 16.7,40.2 20.7,40.2z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
1
public/markdown/monitoring.md
Normal file
1
public/markdown/monitoring.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Monitor your system with Grafana
|
1
public/markdown/support.md
Normal file
1
public/markdown/support.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Access documentation website
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"local>stackspin/renovate-config"
|
"local>stackspin/renovate-config:default"
|
||||||
],
|
],
|
||||||
"npm": {
|
"npm": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
|
|
16
src/App.tsx
16
src/App.tsx
|
@ -3,13 +3,16 @@ import { Helmet } from 'react-helmet';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import { Layout } from './components';
|
import { useAuth } from 'src/services/auth';
|
||||||
import { Dashboard } from './modules';
|
import { Dashboard } from './modules';
|
||||||
|
import { Layout } from './components';
|
||||||
import { AppIframe } from './modules/dashboard/AppIframe';
|
import { AppIframe } from './modules/dashboard/AppIframe';
|
||||||
import { Login } from './modules/login';
|
|
||||||
import { LoginCallback } from './modules/login/LoginCallback';
|
import { LoginCallback } from './modules/login/LoginCallback';
|
||||||
import { useApps } from './services/apps';
|
import { useApps } from './services/apps';
|
||||||
import { useAuth } from './services/auth';
|
import { Login } from './modules/login';
|
||||||
|
import { Users } from './modules/users/Users';
|
||||||
|
import { AppSingle } from './modules/apps/AppSingle';
|
||||||
|
import { Apps } from './modules/apps/Apps';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -53,6 +56,13 @@ function App() {
|
||||||
{apps.map((app) => (
|
{apps.map((app) => (
|
||||||
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
|
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
|
||||||
))}
|
))}
|
||||||
|
<Route path="/users" element={<ProtectedRoute />}>
|
||||||
|
<Route index element={<Users />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/apps" element={<ProtectedRoute />}>
|
||||||
|
<Route path=":slug" element={<AppSingle />} />
|
||||||
|
<Route index element={<Apps />} />
|
||||||
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/dashboard" />} />
|
<Route path="*" element={<Navigate to="/dashboard" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
43
src/components/Form/Checkbox/Checkbox.tsx
Normal file
43
src/components/Form/Checkbox/Checkbox.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
|
||||||
|
/* eslint-disable react/react-in-jsx-scope */
|
||||||
|
export const Checkbox = ({ control, name, label, ...props }: CheckboxProps) => {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
// fieldState: { invalid, isTouched, isDirty },
|
||||||
|
// formState: { touchedFields, dirtyFields },
|
||||||
|
} = useController({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={name}
|
||||||
|
onChange={field.onChange} // send value to hook form
|
||||||
|
onBlur={field.onBlur} // notify when input is touched/blur
|
||||||
|
checked={field.value}
|
||||||
|
name={name} // send down the checkbox name
|
||||||
|
className="shadow-sm focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CheckboxProps = {
|
||||||
|
control: any;
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
1
src/components/Form/Checkbox/index.ts
Normal file
1
src/components/Form/Checkbox/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Checkbox } from './Checkbox';
|
39
src/components/Form/CodeEditor/CodeEditor.tsx
Normal file
39
src/components/Form/CodeEditor/CodeEditor.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { highlight, languages } from 'prismjs';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
import Editor from 'react-simple-code-editor';
|
||||||
|
|
||||||
|
/* eslint-disable react/react-in-jsx-scope */
|
||||||
|
export const CodeEditor = ({ control, name, required, disabled = false }: CodeEditorProps) => {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
// fieldState: { invalid, isTouched, isDirty },
|
||||||
|
// formState: { touchedFields, dirtyFields },
|
||||||
|
} = useController({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
rules: { required },
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Editor
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
highlight={(value) => highlight(value, languages.js, 'yaml')}
|
||||||
|
preClassName="font-mono whitespace-normal font-light"
|
||||||
|
textareaClassName="font-mono overflow-auto font-light"
|
||||||
|
className="font-mono text-sm font-light"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeEditorProps = {
|
||||||
|
control: any;
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
1
src/components/Form/CodeEditor/index.ts
Normal file
1
src/components/Form/CodeEditor/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { CodeEditor } from './CodeEditor';
|
51
src/components/Form/Switch/Switch.tsx
Normal file
51
src/components/Form/Switch/Switch.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useController } from 'react-hook-form';
|
||||||
|
import { Switch as HeadlessSwitch } from '@headlessui/react';
|
||||||
|
|
||||||
|
function classNames(...classes: any) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
/* eslint-disable react/react-in-jsx-scope */
|
||||||
|
export const Switch = ({ control, name, label }: SwitchProps) => {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
// fieldState: { invalid, isTouched, isDirty },
|
||||||
|
// formState: { touchedFields, dirtyFields },
|
||||||
|
} = useController({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<HeadlessSwitch
|
||||||
|
checked={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'bg-primary-600' : 'bg-gray-200',
|
||||||
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</HeadlessSwitch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SwitchProps = {
|
||||||
|
control: any;
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
1
src/components/Form/Switch/index.ts
Normal file
1
src/components/Form/Switch/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Switch } from './Switch';
|
|
@ -1,3 +1,6 @@
|
||||||
export { Input } from './Input';
|
export { Input } from './Input';
|
||||||
export { Select } from './Select';
|
export { Select } from './Select';
|
||||||
|
export { Switch } from './Switch';
|
||||||
|
export { CodeEditor } from './CodeEditor';
|
||||||
export { TextArea } from './TextArea';
|
export { TextArea } from './TextArea';
|
||||||
|
export { Checkbox } from './Checkbox';
|
||||||
|
|
|
@ -1,17 +1,67 @@
|
||||||
import React from 'react';
|
import React, { Fragment, useMemo, useState } from 'react';
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||||
import clsx from 'clsx';
|
import { useAuth } from 'src/services/auth';
|
||||||
|
import Gravatar from 'react-gravatar';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { useApps } from 'src/services/apps';
|
import { useApps } from 'src/services/apps';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { UserModal } from '../UserModal';
|
||||||
|
|
||||||
|
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
|
||||||
|
{ name: 'Users', to: '/users', requiresAdmin: true },
|
||||||
|
{ name: 'Apps', to: '/apps', requiresAdmin: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
function classNames(...classes: any[]) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNavigationByDashboardRole(isAdmin: boolean) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation.filter((item) => !item.requiresAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
interface HeaderProps {}
|
interface HeaderProps {}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = () => {
|
const Header: React.FC<HeaderProps> = () => {
|
||||||
|
const [currentUserModal, setCurrentUserModal] = useState(false);
|
||||||
|
const [currentUserId, setCurrentUserId] = useState(null);
|
||||||
|
const { logOut, currentUser, isAdmin } = useAuth();
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { apps, loadApps, appTableLoading } = useApps();
|
const { apps, loadApps, appTableLoading } = useApps();
|
||||||
|
|
||||||
|
const currentUserModalOpen = (id: any) => {
|
||||||
|
setCurrentUserId(id);
|
||||||
|
setCurrentUserModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentUserModalClose = () => {
|
||||||
|
setCurrentUserModal(false);
|
||||||
|
setCurrentUserId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigationItems = filterNavigationByDashboardRole(isAdmin);
|
||||||
|
|
||||||
|
const signOutUrl = useMemo(() => {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
// If we are developing locally, we need to use the init cluster's public URL
|
||||||
|
if (hostname === 'localhost') {
|
||||||
|
return HYDRA_LOGOUT_URL;
|
||||||
|
}
|
||||||
|
return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||||
|
@ -54,14 +104,64 @@ const Header: React.FC<HeaderProps> = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||||
|
{/* Profile dropdown */}
|
||||||
|
<Menu as="div" className="ml-3 relative">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||||
|
<span className="sr-only">Open user menu</span>
|
||||||
|
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
||||||
|
<Gravatar email={currentUser?.email || undefined} size={32} rating="pg" protocol="https://" />
|
||||||
|
</span>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => currentUserModalOpen(currentUser?.id)}
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Configure profile
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => logOut()}
|
||||||
|
href={signOutUrl}
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Disclosure.Panel className="sm:hidden">
|
<Disclosure.Panel className="sm:hidden">
|
||||||
<div className="pt-2 pb-4 space-y-1">
|
<div className="pt-2 pb-4 space-y-1">
|
||||||
{
|
{apps.map((app) => (
|
||||||
// @ts-ignore
|
|
||||||
apps.map((app) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={app.name}
|
key={app.name}
|
||||||
to={app.slug}
|
to={app.slug}
|
||||||
|
@ -74,13 +174,16 @@ const Header: React.FC<HeaderProps> = () => {
|
||||||
>
|
>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Link>
|
</Link>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
|
|
||||||
|
{currentUserModal && (
|
||||||
|
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,9 +7,11 @@ export const Modal: React.FC<ModalProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
|
saveButtonTitle = 'Save Changes',
|
||||||
children,
|
children,
|
||||||
title = '',
|
title = '',
|
||||||
useCancelButton = false,
|
useCancelButton = false,
|
||||||
|
cancelButtonTitle = 'Cancel',
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
leftActions = <></>,
|
leftActions = <></>,
|
||||||
saveButtonDisabled = false,
|
saveButtonDisabled = false,
|
||||||
|
@ -94,7 +96,7 @@ export const Modal: React.FC<ModalProps> = ({
|
||||||
ref={saveButtonRef}
|
ref={saveButtonRef}
|
||||||
disabled={saveButtonDisabled}
|
disabled={saveButtonDisabled}
|
||||||
>
|
>
|
||||||
Save Changes
|
{saveButtonTitle}
|
||||||
</button>
|
</button>
|
||||||
{useCancelButton && (
|
{useCancelButton && (
|
||||||
<button
|
<button
|
||||||
|
@ -103,7 +105,7 @@ export const Modal: React.FC<ModalProps> = ({
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
ref={cancelButtonRef}
|
ref={cancelButtonRef}
|
||||||
>
|
>
|
||||||
Cancel
|
{cancelButtonTitle}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,9 @@ export type ModalProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
|
saveButtonTitle?: string;
|
||||||
useCancelButton?: boolean;
|
useCancelButton?: boolean;
|
||||||
|
cancelButtonTitle?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
leftActions?: React.ReactNode;
|
leftActions?: React.ReactNode;
|
||||||
saveButtonDisabled?: boolean;
|
saveButtonDisabled?: boolean;
|
||||||
|
|
|
@ -166,7 +166,7 @@ export const Table = <T extends Record<string, unknown>>({
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-primary-600 mt-2">Loading users</p>
|
<p className="text-sm text-primary-600 mt-2">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -2,11 +2,14 @@ import React, { useState } from 'react';
|
||||||
import { TabPanel } from './TabPanel';
|
import { TabPanel } from './TabPanel';
|
||||||
import { TabsProps } from './types';
|
import { TabsProps } from './types';
|
||||||
|
|
||||||
export const Tabs = ({ tabs }: TabsProps) => {
|
export const Tabs = ({ tabs, onTabClick }: TabsProps) => {
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
||||||
|
|
||||||
const handleTabPress = (index: number) => () => {
|
const handleTabPress = (index: number) => () => {
|
||||||
setActiveTabIndex(index);
|
setActiveTabIndex(index);
|
||||||
|
if (onTabClick) {
|
||||||
|
onTabClick(index);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function classNames(...classes: any) {
|
function classNames(...classes: any) {
|
||||||
|
@ -23,7 +26,6 @@ export const Tabs = ({ tabs }: TabsProps) => {
|
||||||
id="tabs"
|
id="tabs"
|
||||||
name="tabs"
|
name="tabs"
|
||||||
className="block w-full focus:ring-primary-500 focus:border-primary-500 border-gray-300 rounded-md"
|
className="block w-full focus:ring-primary-500 focus:border-primary-500 border-gray-300 rounded-md"
|
||||||
// defaultValue={tabs ? tabs.find((tab) => tab.current).name : undefined}
|
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<option key={tab.name}>{tab.name}</option>
|
<option key={tab.name}>{tab.name}</option>
|
||||||
|
|
|
@ -6,6 +6,7 @@ type Tab = {
|
||||||
|
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
|
onTabClick?: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabPanelProps {
|
export interface TabPanelProps {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Banner, Modal, ConfirmationModal } from 'src/components';
|
||||||
import { Input, Select } from 'src/components/Form';
|
import { Input, Select } from 'src/components/Form';
|
||||||
import { User, UserRole, useUsers } from 'src/services/users';
|
import { User, UserRole, useUsers } from 'src/services/users';
|
||||||
import { useAuth } from 'src/services/auth';
|
import { useAuth } from 'src/services/auth';
|
||||||
|
import { HIDDEN_APPS } from 'src/modules/dashboard/consts';
|
||||||
import { appAccessList, initialUserForm } from './consts';
|
import { appAccessList, initialUserForm } from './consts';
|
||||||
import { UserModalProps } from './types';
|
import { UserModalProps } from './types';
|
||||||
|
|
||||||
|
@ -221,7 +222,7 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps)
|
||||||
<div className="flow-root mt-6">
|
<div className="flow-root mt-6">
|
||||||
<ul className="-my-5 divide-y divide-gray-200">
|
<ul className="-my-5 divide-y divide-gray-200">
|
||||||
{fields.map((item, index) => {
|
{fields.map((item, index) => {
|
||||||
if (item.name === 'dashboard') {
|
if (item.name != null && HIDDEN_APPS.indexOf(item.name) !== -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,31 @@ export const appAccessList = [
|
||||||
name: 'wekan',
|
name: 'wekan',
|
||||||
image: '/assets/wekan.svg',
|
image: '/assets/wekan.svg',
|
||||||
label: 'Wekan',
|
label: 'Wekan',
|
||||||
|
documentationUrl: 'https://github.com/wekan/wekan/wiki',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'wordpress',
|
name: 'wordpress',
|
||||||
image: '/assets/wordpress.svg',
|
image: '/assets/wordpress.svg',
|
||||||
label: 'Wordpress',
|
label: 'Wordpress',
|
||||||
|
documentationUrl: 'https://wordpress.org/support/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'nextcloud',
|
name: 'nextcloud',
|
||||||
image: '/assets/nextcloud.svg',
|
image: '/assets/nextcloud.svg',
|
||||||
label: 'Nextcloud',
|
label: 'Nextcloud',
|
||||||
|
documentationUrl: 'https://docs.nextcloud.com/server/latest/user_manual/en/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zulip',
|
name: 'zulip',
|
||||||
image: '/assets/zulip.svg',
|
image: '/assets/zulip.svg',
|
||||||
label: 'Zulip',
|
label: 'Zulip',
|
||||||
|
documentationUrl: 'https://docs.zulip.com/help/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'monitoring',
|
||||||
|
image: '/assets/monitoring.svg',
|
||||||
|
label: 'Monitoring',
|
||||||
|
documentationUrl: 'https://grafana.com/docs/',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -53,6 +63,10 @@ export const initialAppRoles = [
|
||||||
name: 'zulip',
|
name: 'zulip',
|
||||||
role: UserRole.User,
|
role: UserRole.User,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'monitoring',
|
||||||
|
role: UserRole.NoAccess,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const initialUserForm = {
|
export const initialUserForm = {
|
||||||
|
|
173
src/modules/apps/AppSingle.tsx
Normal file
173
src/modules/apps/AppSingle.tsx
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* This page shows information about a single application. It contains several
|
||||||
|
* configuration options (that are not implemented in the back-end yet) such as:
|
||||||
|
*
|
||||||
|
* 1. Toggling auto-updates
|
||||||
|
* 2. Advanced configuration by overwriting helm values
|
||||||
|
* 3. Deleting the application
|
||||||
|
*
|
||||||
|
* This page is only available for Admin users.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline';
|
||||||
|
import { DisableAppForm, useApps } from 'src/services/apps';
|
||||||
|
import { Modal, Tabs } from 'src/components';
|
||||||
|
import { Checkbox } from 'src/components/Form';
|
||||||
|
import { appAccessList } from 'src/components/UserModal/consts';
|
||||||
|
import { AdvancedTab, GeneralTab } from './components';
|
||||||
|
|
||||||
|
export const AppSingle: React.FC = () => {
|
||||||
|
const [disableAppModal, setDisableAppModal] = useState(false);
|
||||||
|
const [removeAppData, setRemoveAppData] = useState(false);
|
||||||
|
const params = useParams();
|
||||||
|
const appSlug = params.slug;
|
||||||
|
const { app, loadApp, disableApp, clearSelectedApp } = useApps();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const initialDisableData = { slug: appSlug, removeAppData: false };
|
||||||
|
|
||||||
|
const { control, reset, handleSubmit } = useForm<DisableAppForm>({
|
||||||
|
defaultValues: initialDisableData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeAppDataWatch = useWatch({
|
||||||
|
control,
|
||||||
|
name: 'removeAppData',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRemoveAppData(removeAppDataWatch);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [removeAppDataWatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appSlug) {
|
||||||
|
loadApp(appSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearSelectedApp();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appSlug]);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appImageSrc = _.find(appAccessList, { name: appSlug })?.image;
|
||||||
|
const appDocumentationUrl = _.find(appAccessList, { name: appSlug })?.documentationUrl;
|
||||||
|
|
||||||
|
const openDocumentationInNewTab = () => {
|
||||||
|
window.open(appDocumentationUrl, '_blank', 'noopener,noreferrer');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: 'General',
|
||||||
|
component: <GeneralTab />,
|
||||||
|
},
|
||||||
|
{ name: 'Advanced Configuration', component: <AdvancedTab /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onDisableApp = async () => {
|
||||||
|
try {
|
||||||
|
await handleSubmit((data) => disableApp(data))();
|
||||||
|
} catch (e: any) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
setDisableAppModal(false);
|
||||||
|
clearSelectedApp();
|
||||||
|
navigate('/apps');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDisableModal = () => {
|
||||||
|
reset(initialDisableData);
|
||||||
|
setDisableAppModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden lg:flex lg:flex-row">
|
||||||
|
<div
|
||||||
|
className="block bg-white overflow-hidden shadow rounded-sm basis-4/12 mx-auto sm:px-6 lg:px-8 overflow-hidden block lg:flex-none"
|
||||||
|
style={{ height: 'fit-content' }}
|
||||||
|
>
|
||||||
|
<div className="px-4 py-5 sm:p-6 flex flex-col">
|
||||||
|
<img className="h-24 w-24 rounded-md overflow-hidden mr-4 mb-3" src={appImageSrc} alt={app.name} />
|
||||||
|
<div className="mb-3">
|
||||||
|
<h2 className="text-2xl leading-8 font-bold">{app.name}</h2>
|
||||||
|
<div className="text-sm leading-5 font-medium text-gray-500">Installed on August 25, 2020</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
|
||||||
|
onClick={() => setDisableAppModal(true)}
|
||||||
|
>
|
||||||
|
<XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" />
|
||||||
|
Disable App
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
|
||||||
|
onClick={openDocumentationInNewTab}
|
||||||
|
>
|
||||||
|
View manual
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto sm:mt-8 lg:mt-0 lg:px-8 overflow-hidden block">
|
||||||
|
<div className="bg-white sm:px-6 shadow rounded-sm basis-8/12">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<Tabs tabs={tabs} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{disableAppModal && (
|
||||||
|
<Modal
|
||||||
|
onClose={handleCloseDisableModal}
|
||||||
|
open={disableAppModal}
|
||||||
|
onSave={onDisableApp}
|
||||||
|
saveButtonTitle={removeAppData ? `Yes, delete and it's data` : 'Yes, delete'}
|
||||||
|
cancelButtonTitle="No, cancel"
|
||||||
|
useCancelButton
|
||||||
|
>
|
||||||
|
<div className="bg-white px-4">
|
||||||
|
<div className="space-y-10 divide-y divide-gray-200">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Disable app</h3>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
Are you sure you want to disable {app.name}? The app will get uninstalled and none of your users will
|
||||||
|
be able to access the app.
|
||||||
|
</div>
|
||||||
|
<fieldset className="px-4 py-5 sm:p-6">
|
||||||
|
<div className="relative flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<Checkbox control={control} name="removeAppData" id="removeAppData" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="removeAppData" className="font-medium text-gray-700">
|
||||||
|
Remove app data
|
||||||
|
</label>
|
||||||
|
<p id="removeAppData-description" className="text-gray-500">
|
||||||
|
{removeAppData
|
||||||
|
? `The app's data will be removed. After this operation is done you will not be able to access the app, nor the app data. If you re-install the app, it will have none of the data it had before.`
|
||||||
|
: `The app's data does not get removed. If you install the app again, you will be able to access the data again.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
178
src/modules/apps/Apps.tsx
Normal file
178
src/modules/apps/Apps.tsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
/**
|
||||||
|
* This page shows all the applications and their status in a table.
|
||||||
|
*
|
||||||
|
* This page is only available for Admin users.
|
||||||
|
*/
|
||||||
|
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
|
// import { useNavigate } from 'react-router';
|
||||||
|
import { SearchIcon } from '@heroicons/react/solid';
|
||||||
|
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||||
|
import _, { debounce } from 'lodash';
|
||||||
|
import { Table } from 'src/components';
|
||||||
|
import { App, AppStatusEnum, useApps } from 'src/services/apps';
|
||||||
|
// import { AppInstallModal } from './components';
|
||||||
|
import { getConstForStatus } from './consts';
|
||||||
|
|
||||||
|
export const Apps: React.FC = () => {
|
||||||
|
// If you want to enable the App Install button again, uncomment this:
|
||||||
|
// const [installModalOpen, setInstallModalOpen] = useState(false);
|
||||||
|
// const [appSlug, setAppSlug] = useState(null);
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const { apps, appTableLoading, loadApps } = useApps();
|
||||||
|
|
||||||
|
const handleSearch = useCallback((event: any) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadApps();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedSearch.cancel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filterSearch = useMemo(() => {
|
||||||
|
return _.filter(apps, (item) => item.name?.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
}, [apps, search]);
|
||||||
|
|
||||||
|
const columns: any = [
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
Cell: (e: any) => {
|
||||||
|
const app = e.cell.row.original as App;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0 h-10 w-10">
|
||||||
|
<img className="h-10 w-10 rounded-md overflow-hidden" src={app.assetSrc} alt={app.name} />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-sm font-medium text-gray-900">{app.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: 'status',
|
||||||
|
Cell: (e: any) => {
|
||||||
|
const status = e.cell.row.original.status as AppStatusEnum;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} />
|
||||||
|
{status === AppStatusEnum.Installing ? (
|
||||||
|
<div
|
||||||
|
className={`ml-2 cursor-pointer text-sm text-${getConstForStatus(status, 'colorClass')}`}
|
||||||
|
onClick={() => showToast('Installing an app can take up to 10 minutes.', ToastType.Success)}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={`ml-2 text-sm text-${getConstForStatus(status, 'colorClass')}`}>{status}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
// If you want to enable the App Install button again, uncomment this:
|
||||||
|
//
|
||||||
|
// We need to implement installation and configuration in the back-end to be
|
||||||
|
// able to use those buttons.
|
||||||
|
// {
|
||||||
|
// Header: ' ',
|
||||||
|
// Cell: (e: any) => {
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
// const appStatus = e.cell.row.original.status as AppStatusEnum;
|
||||||
|
// if (appStatus === AppStatusEnum.Installing) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// const { slug } = e.cell.row.original;
|
||||||
|
// let buttonFunction = () => navigate(`/apps/${slug}`);
|
||||||
|
// if (appStatus === AppStatusEnum.NotInstalled) {
|
||||||
|
// buttonFunction = () => {
|
||||||
|
// setAppSlug(slug);
|
||||||
|
// setInstallModalOpen(true);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return (
|
||||||
|
// <div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
// <button
|
||||||
|
// onClick={buttonFunction}
|
||||||
|
// type="button"
|
||||||
|
// className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
// >
|
||||||
|
// {getConstForStatus(appStatus, 'buttonIcon')}
|
||||||
|
// {getConstForStatus(appStatus, 'buttonTitle')}
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// width: 'auto',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||||
|
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<h1 className="text-3xl leading-6 font-bold text-gray-900">Apps</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between w-100 my-3 items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="inline-block">
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
|
||||||
|
Search candidates
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 flex rounded-md shadow-sm">
|
||||||
|
<div className="relative flex items-stretch flex-grow focus-within:z-10">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
className="focus:ring-primary-dark focus:border-primary-dark block w-full rounded-md pl-10 sm:text-sm border-gray-200"
|
||||||
|
placeholder="Search Apps"
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="-my-2 sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="shadow border-b border-gray-200 sm:rounded-lg">
|
||||||
|
<Table
|
||||||
|
data={_.filter(filterSearch, (app) => app.slug !== 'dashboard') as any}
|
||||||
|
columns={columns}
|
||||||
|
loading={appTableLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
// If you want to enable the App Install button again, uncomment this:
|
||||||
|
//
|
||||||
|
// installModalOpen && (
|
||||||
|
// <AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
180
src/modules/apps/components/AdvancedTab/AdvancedTab.tsx
Normal file
180
src/modules/apps/components/AdvancedTab/AdvancedTab.tsx
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import Editor from 'react-simple-code-editor';
|
||||||
|
// import { Menu, Transition } from '@headlessui/react';
|
||||||
|
// import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { highlight, languages } from 'prismjs';
|
||||||
|
import 'prismjs/components/prism-clike';
|
||||||
|
import 'prismjs/components/prism-yaml';
|
||||||
|
import 'prismjs/themes/prism.css';
|
||||||
|
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||||
|
import { useApps } from 'src/services/apps';
|
||||||
|
import { initialEditorYaml } from '../../consts';
|
||||||
|
|
||||||
|
export const AdvancedTab = () => {
|
||||||
|
const [code, setCode] = React.useState(initialEditorYaml);
|
||||||
|
const { app, editApp } = useApps();
|
||||||
|
|
||||||
|
const resetCode = () => {
|
||||||
|
setCode(initialEditorYaml);
|
||||||
|
showToast('Code was reset.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isConfigurationValid = () => {
|
||||||
|
try {
|
||||||
|
yaml.load(code);
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyCode = () => {
|
||||||
|
if (isConfigurationValid()) {
|
||||||
|
showToast('Configuration is valid.', ToastType.Success);
|
||||||
|
} else {
|
||||||
|
showToast('Configuration is not valid! Please fix configuration issues and try again.', ToastType.Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
if (isConfigurationValid()) {
|
||||||
|
editApp({ ...app, configuration: code });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast('Configuration is not valid! Please fix configuration issues and try again.', ToastType.Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between mb-5">
|
||||||
|
<h1 className="text-2xl leading-6 font-medium text-gray-900">Configuration</h1>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-flow-col grid-cols-2 gap-8">
|
||||||
|
<div className="bg-gray-100 overflow-hidden rounded-lg">
|
||||||
|
<div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
|
||||||
|
<span className="text-gray-600 text-lg leading-6 font-medium">Current Configuration</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-5 sm:p-6 overflow-x-auto">
|
||||||
|
<Editor
|
||||||
|
value={initialEditorYaml}
|
||||||
|
onValueChange={_.noop}
|
||||||
|
highlight={(value) => highlight(value, languages.js, 'yaml')}
|
||||||
|
preClassName="font-mono text-sm font-light"
|
||||||
|
textareaClassName="font-mono overflow-auto font-light"
|
||||||
|
className="font-mono text-sm font-light"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden rounded-lg">
|
||||||
|
{/* <div className="px-4 h-16 sm:px-6 bg-gray-200 flex justify-between items-center rounded-t-lg">
|
||||||
|
<span className="text-gray-600 text-lg leading-6 font-medium">Edit Configuration</span>
|
||||||
|
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-primary-500">
|
||||||
|
Versions
|
||||||
|
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="z-10 origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div className="py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||||
|
'block px-4 py-2 text-sm',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Save Version
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||||
|
'block px-4 py-2 text-sm',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View Version History
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||||
|
'block px-4 py-2 text-sm',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Restore Current Version
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div> */}
|
||||||
|
<div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
|
||||||
|
<span className="text-gray-600 text-lg leading-6 font-medium">Edit Configuration</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-5 sm:p-6 border border-t-0 border-gray-200">
|
||||||
|
<Editor
|
||||||
|
value={code}
|
||||||
|
onValueChange={(value) => setCode(value)}
|
||||||
|
highlight={(value) => highlight(value, languages.js, 'yaml')}
|
||||||
|
preClassName="font-mono whitespace-normal font-light"
|
||||||
|
textareaClassName="font-mono overflow-auto font-light"
|
||||||
|
className="font-mono text-sm font-light"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={resetCode}
|
||||||
|
className="mr-3 inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={verifyCode}
|
||||||
|
className="mr-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveChanges}
|
||||||
|
className="inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { App, useApps } from 'src/services/apps';
|
||||||
|
import { Modal } from 'src/components';
|
||||||
|
import { Input } from 'src/components/Form';
|
||||||
|
import { AppInstallModalProps } from './types';
|
||||||
|
import { initialAppForm, initialCode } from './consts';
|
||||||
|
|
||||||
|
export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => {
|
||||||
|
const [appName, setAppName] = useState('');
|
||||||
|
const { app, appLoading, installApp, loadApp, clearSelectedApp } = useApps();
|
||||||
|
|
||||||
|
const { control, reset, handleSubmit } = useForm<App>({
|
||||||
|
defaultValues: initialAppForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appSlug) {
|
||||||
|
loadApp(appSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(initialAppForm);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appSlug, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!_.isEmpty(app)) {
|
||||||
|
setAppName(app.name);
|
||||||
|
reset({ url: app.url, configuration: initialCode, slug: appSlug ?? '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset({ url: initialAppForm.url, configuration: initialCode, slug: appSlug ?? '' });
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [app, reset, open]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
clearSelectedApp();
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await handleSubmit((data) => installApp(data))();
|
||||||
|
} catch (e: any) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: any) => {
|
||||||
|
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={appLoading} useCancelButton>
|
||||||
|
<div className="bg-white px-4">
|
||||||
|
<div className="space-y-10 divide-y divide-gray-200">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Install app {appName}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<Input control={control} name="url" label="URL" onKeyPress={handleKeyPress} required={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
27
src/modules/apps/components/AppInstallModal/consts.ts
Normal file
27
src/modules/apps/components/AppInstallModal/consts.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { App } from 'src/services/apps';
|
||||||
|
|
||||||
|
export const initialCode = `luck: except
|
||||||
|
natural: still
|
||||||
|
near: though
|
||||||
|
search:
|
||||||
|
- feature
|
||||||
|
- - 1980732354.689713
|
||||||
|
- hour
|
||||||
|
- butter:
|
||||||
|
ordinary: 995901949.8974948
|
||||||
|
teeth: true
|
||||||
|
whole:
|
||||||
|
- -952367353
|
||||||
|
- - talk: -1773961379
|
||||||
|
temperature: false
|
||||||
|
oxygen: true
|
||||||
|
laugh:
|
||||||
|
flag:
|
||||||
|
in: 2144751662
|
||||||
|
hospital: -1544066384.1973226
|
||||||
|
law: congress
|
||||||
|
great: stomach`;
|
||||||
|
|
||||||
|
export const initialAppForm = {
|
||||||
|
url: '',
|
||||||
|
} as App;
|
1
src/modules/apps/components/AppInstallModal/index.ts
Normal file
1
src/modules/apps/components/AppInstallModal/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { AppInstallModal } from './AppInstallModal';
|
5
src/modules/apps/components/AppInstallModal/types.ts
Normal file
5
src/modules/apps/components/AppInstallModal/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type AppInstallModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
appSlug: string | null;
|
||||||
|
};
|
59
src/modules/apps/components/GeneralTab/GeneralTab.tsx
Normal file
59
src/modules/apps/components/GeneralTab/GeneralTab.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import { Switch } from 'src/components/Form';
|
||||||
|
import { App, useApps } from 'src/services/apps';
|
||||||
|
|
||||||
|
export const GeneralTab = () => {
|
||||||
|
const { app, editApp } = useApps();
|
||||||
|
const { control, reset, handleSubmit } = useForm<App>({
|
||||||
|
defaultValues: { automaticUpdates: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!_.isEmpty(app)) {
|
||||||
|
reset(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset({ automaticUpdates: false });
|
||||||
|
};
|
||||||
|
}, [app, reset]);
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<App> = (data) => {
|
||||||
|
try {
|
||||||
|
editApp(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="md:grid md:grid-cols-3 md:gap-6">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">Automatic updates</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et nibh sit amet mauris faucibus molestie
|
||||||
|
gravida at orci.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="my-auto ml-auto">
|
||||||
|
<Switch control={control} name="automaticUpdates" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-3 sm:flex">
|
||||||
|
<div className="ml-auto sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
>
|
||||||
|
Save changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
3
src/modules/apps/components/index.ts
Normal file
3
src/modules/apps/components/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { GeneralTab } from './GeneralTab/GeneralTab';
|
||||||
|
export { AdvancedTab } from './AdvancedTab/AdvancedTab';
|
||||||
|
export { AppInstallModal } from './AppInstallModal';
|
52
src/modules/apps/consts.tsx
Normal file
52
src/modules/apps/consts.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { CogIcon, PlusCircleIcon } from '@heroicons/react/outline';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { AppStatusEnum } from 'src/services/apps';
|
||||||
|
|
||||||
|
export const initialEditorYaml = `luck: except
|
||||||
|
natural: still
|
||||||
|
near: though
|
||||||
|
search:
|
||||||
|
- feature
|
||||||
|
- - 1980732354.689713
|
||||||
|
- hour
|
||||||
|
- butter:
|
||||||
|
ordinary: 995901949.8974948
|
||||||
|
teeth: true
|
||||||
|
whole:
|
||||||
|
- -952367353
|
||||||
|
- - talk: -1773961379
|
||||||
|
temperature: false
|
||||||
|
oxygen: true
|
||||||
|
laugh:
|
||||||
|
flag:
|
||||||
|
in: 2144751662
|
||||||
|
hospital: -1544066384.1973226
|
||||||
|
law: congress
|
||||||
|
great: stomach`;
|
||||||
|
|
||||||
|
const tableConsts = [
|
||||||
|
{
|
||||||
|
status: AppStatusEnum.Installed,
|
||||||
|
colorClass: 'green-600',
|
||||||
|
buttonTitle: 'Configure',
|
||||||
|
buttonIcon: <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: AppStatusEnum.NotInstalled,
|
||||||
|
colorClass: 'gray-600',
|
||||||
|
buttonTitle: 'Install',
|
||||||
|
buttonIcon: <PlusCircleIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: AppStatusEnum.Installing,
|
||||||
|
colorClass: 'primary-600',
|
||||||
|
buttonTitle: null,
|
||||||
|
buttonIcon: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getConstForStatus = (appStatus: AppStatusEnum, paramName: string) => {
|
||||||
|
const tableConst = _.find(tableConsts, { status: appStatus });
|
||||||
|
return _.get(tableConst, paramName);
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
export const DashboardUtility: React.FC<any> = ({ item }: { item: any }) => {
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(item.markdownSrc)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((md) => {
|
||||||
|
return setContent(md);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [item.markdownSrc]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
key={item.name}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
|
||||||
|
{item.icon && <item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />}
|
||||||
|
{item.assetSrc && <img className="h-6 w-6" src={item.assetSrc} alt={item.name} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
|
||||||
|
<dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">
|
||||||
|
<ReactMarkdown>{content}</ReactMarkdown>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export { DashboardUtility } from './DashboardUtility';
|
|
@ -1 +1,2 @@
|
||||||
export { DashboardCard } from './DashboardCard';
|
export { DashboardCard } from './DashboardCard';
|
||||||
|
export { DashboardUtility } from './DashboardUtility';
|
||||||
|
|
16
src/modules/dashboard/consts.ts
Normal file
16
src/modules/dashboard/consts.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { InformationCircleIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
export const DASHBOARD_QUICK_ACCESS = [
|
||||||
|
{
|
||||||
|
name: 'Support',
|
||||||
|
url: 'https://docs.stackspin.net',
|
||||||
|
markdownSrc: '/markdown/support.md',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Apps that should not be shown on the dashboard */
|
||||||
|
export const HIDDEN_APPS = ['dashboard', 'velero'];
|
||||||
|
|
||||||
|
/** Apps that should be shown under "Utilities" */
|
||||||
|
export const UTILITY_APPS = ['monitoring'];
|
190
src/modules/users/Users.tsx
Normal file
190
src/modules/users/Users.tsx
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
/**
|
||||||
|
* This page shows a table of all users. It is only available for Admin users.
|
||||||
|
*
|
||||||
|
* Admin users can add one or more users, or edit a user.
|
||||||
|
*/
|
||||||
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
|
||||||
|
import { CogIcon, TrashIcon } from '@heroicons/react/outline';
|
||||||
|
import { useUsers } from 'src/services/users';
|
||||||
|
import { Table } from 'src/components';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { useAuth } from 'src/services/auth';
|
||||||
|
|
||||||
|
import { UserModal } from '../../components/UserModal';
|
||||||
|
// import { MultipleUsersModal } from '../components/multipleUsersModal';
|
||||||
|
|
||||||
|
export const Users: React.FC = () => {
|
||||||
|
const [selectedRowsIds, setSelectedRowsIds] = useState({});
|
||||||
|
const [configureModal, setConfigureModal] = useState(false);
|
||||||
|
const [multipleUsersModal, setMultipleUsersModal] = useState(false);
|
||||||
|
const [userId, setUserId] = useState(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const { users, loadUsers, userTableLoading } = useUsers();
|
||||||
|
const { isAdmin } = useAuth();
|
||||||
|
|
||||||
|
const handleSearch = (event: any) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUsers();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedSearch.cancel();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filterSearch = useMemo(() => {
|
||||||
|
return users.filter((item: any) => item.email?.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
}, [search, users]);
|
||||||
|
|
||||||
|
const configureModalOpen = (id: any) => {
|
||||||
|
setUserId(id);
|
||||||
|
setConfigureModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configureModalClose = () => setConfigureModal(false);
|
||||||
|
|
||||||
|
const multipleUsersModalClose = () => setMultipleUsersModal(false);
|
||||||
|
|
||||||
|
const columns: any = React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Email',
|
||||||
|
accessor: 'email',
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: 'status',
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: ' ',
|
||||||
|
Cell: (props: any) => {
|
||||||
|
const { row } = props;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={() => configureModalOpen(row.original.id)}
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Configure
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[isAdmin],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedRows = useCallback((rows: Record<string, boolean>) => {
|
||||||
|
setSelectedRowsIds(rows);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||||
|
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
onClick={() => configureModalOpen(null)}
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
|
||||||
|
>
|
||||||
|
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Add new user
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMultipleUsersModal(true)}
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
|
||||||
|
>
|
||||||
|
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Add new users
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between w-100 my-3 items-center mb-5 ">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="inline-block">
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
|
||||||
|
Search candidates
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 flex rounded-md shadow-sm">
|
||||||
|
<div className="relative flex items-stretch flex-grow focus-within:z-10">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
className="focus:ring-primary-500 focus:border-primary-500 block w-full rounded-md pl-10 sm:text-sm border-gray-200"
|
||||||
|
placeholder="Search Users"
|
||||||
|
onChange={debouncedSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => {}}
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||||
|
>
|
||||||
|
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
|
||||||
|
<Table
|
||||||
|
data={filterSearch as any}
|
||||||
|
columns={columns}
|
||||||
|
getSelectedRowIds={selectedRows}
|
||||||
|
loading={userTableLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{configureModal && (
|
||||||
|
<UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,9 +6,7 @@ import storage from 'redux-persist/lib/storage';
|
||||||
import { reducer as authReducer } from 'src/services/auth';
|
import { reducer as authReducer } from 'src/services/auth';
|
||||||
|
|
||||||
import usersReducer from 'src/services/users/redux/reducers';
|
import usersReducer from 'src/services/users/redux/reducers';
|
||||||
|
|
||||||
import appsReducer from 'src/services/apps/redux/reducers';
|
import appsReducer from 'src/services/apps/redux/reducers';
|
||||||
|
|
||||||
import { State } from './types';
|
import { State } from './types';
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
|
|
|
@ -1,22 +1,48 @@
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { fetchApps } from '../redux/actions';
|
import { fetchApps, fetchAppBySlug, updateApp, installAppBySlug, clearCurrentApp, deleteApp } from '../redux';
|
||||||
import { getApps } from '../redux';
|
import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors';
|
||||||
import { getAppById, getAppslLoading } from '../redux/selectors';
|
|
||||||
|
|
||||||
export function useApps() {
|
export function useApps() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const apps = useSelector(getApps);
|
const apps = useSelector(getApps);
|
||||||
const app = useSelector(getAppById);
|
const app = useSelector(getCurrentApp);
|
||||||
const appTableLoading = useSelector(getAppslLoading);
|
const appLoading = useSelector(getAppLoading);
|
||||||
|
const appTableLoading = useSelector(getAppsLoading);
|
||||||
|
|
||||||
function loadApps() {
|
function loadApps() {
|
||||||
return dispatch(fetchApps());
|
return dispatch(fetchApps());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadApp(slug: string) {
|
||||||
|
return dispatch(fetchAppBySlug(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
function editApp(data: any) {
|
||||||
|
return dispatch(updateApp(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function installApp(data: any) {
|
||||||
|
return dispatch(installAppBySlug(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableApp(data: any) {
|
||||||
|
return dispatch(deleteApp(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedApp() {
|
||||||
|
return dispatch(clearCurrentApp());
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apps,
|
apps,
|
||||||
app,
|
app,
|
||||||
|
loadApp,
|
||||||
loadApps,
|
loadApps,
|
||||||
|
editApp,
|
||||||
|
appLoading,
|
||||||
appTableLoading,
|
appTableLoading,
|
||||||
|
installApp,
|
||||||
|
disableApp,
|
||||||
|
clearSelectedApp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
export { reducer } from './redux';
|
export { reducer } from './redux';
|
||||||
|
|
||||||
export { useApps } from './hooks';
|
export { useApps } from './hooks';
|
||||||
|
|
||||||
export { fetchApps } from './api';
|
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||||
import { State } from 'src/redux/types';
|
|
||||||
import { performApiCall } from 'src/services/api';
|
import { performApiCall } from 'src/services/api';
|
||||||
import { AuthActionTypes } from 'src/services/auth';
|
import { transformAppRequest, transformApp, transformInstallAppRequest } from '../transformations';
|
||||||
import { transformApp } from '../transformations';
|
|
||||||
|
|
||||||
export enum AppActionTypes {
|
export enum AppActionTypes {
|
||||||
FETCH_APPS = 'apps/fetch_apps',
|
FETCH_APPS = 'apps/fetch_apps',
|
||||||
FETCH_APP = 'apps/fetch_app',
|
FETCH_APP = 'apps/fetch_app',
|
||||||
UPDATE_APP = 'apps/update_app',
|
UPDATE_APP = 'apps/update_app',
|
||||||
CREATE_APP = 'apps/create_app',
|
INSTALL_APP = 'apps/install_app',
|
||||||
DELETE_APP = 'apps/delete_app',
|
DELETE_APP = 'apps/delete_app',
|
||||||
SET_APP_MODAL_LOADING = 'apps/app_modal_loading',
|
CLEAR_APP = 'apps/clear_app',
|
||||||
|
SET_APP_LOADING = 'apps/app_loading',
|
||||||
SET_APPS_LOADING = 'apps/apps_loading',
|
SET_APPS_LOADING = 'apps/apps_loading',
|
||||||
CREATE_BATCH_APPS = 'apps/create_batch_apps',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||||
|
@ -24,9 +21,9 @@ export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch<any>)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAppModalLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
export const setAppLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: AppActionTypes.SET_APP_MODAL_LOADING,
|
type: AppActionTypes.SET_APP_LOADING,
|
||||||
payload: isLoading,
|
payload: isLoading,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -51,12 +48,12 @@ export const fetchApps = () => async (dispatch: Dispatch<any>) => {
|
||||||
dispatch(setAppsLoading(false));
|
dispatch(setAppsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchAPPById = (id: string) => async (dispatch: Dispatch<any>) => {
|
export const fetchAppBySlug = (slug: string) => async (dispatch: Dispatch<any>) => {
|
||||||
dispatch(setAppModalLoading(true));
|
dispatch(setAppLoading(true));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await performApiCall({
|
const { data } = await performApiCall({
|
||||||
path: `/apps/${id}`,
|
path: `/apps/${slug}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,5 +65,91 @@ export const fetchAPPById = (id: string) => async (dispatch: Dispatch<any>) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setAppModalLoading(false));
|
dispatch(setAppLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateApp = (app: any) => async (dispatch: Dispatch<any>) => {
|
||||||
|
dispatch(setAppLoading(true));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await performApiCall({
|
||||||
|
path: `/apps/${app.slug}`,
|
||||||
|
method: 'PUT',
|
||||||
|
body: transformAppRequest(app),
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: AppActionTypes.UPDATE_APP,
|
||||||
|
payload: transformApp(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast('App is updated!', ToastType.Success);
|
||||||
|
|
||||||
|
dispatch(fetchApps());
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setAppLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const installAppBySlug = (app: any) => async (dispatch: Dispatch<any>) => {
|
||||||
|
dispatch(setAppLoading(true));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await performApiCall({
|
||||||
|
path: `/apps/${app.slug}/install`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: transformInstallAppRequest(app),
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: AppActionTypes.INSTALL_APP,
|
||||||
|
payload: transformApp(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast('App installing...', ToastType.Success);
|
||||||
|
|
||||||
|
dispatch(fetchApps());
|
||||||
|
} catch (err: any) {
|
||||||
|
dispatch(setAppLoading(false));
|
||||||
|
showToast(`${err}`, ToastType.Error);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setAppLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteApp = (appData: any) => async (dispatch: Dispatch<any>) => {
|
||||||
|
dispatch(setAppLoading(true));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await performApiCall({
|
||||||
|
path: `/apps/${appData.slug}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
body: { remove_app_data: appData.removeAppData },
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: AppActionTypes.DELETE_APP,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast('App disabled', ToastType.Success);
|
||||||
|
|
||||||
|
dispatch(fetchApps());
|
||||||
|
} catch (err: any) {
|
||||||
|
dispatch(setAppLoading(false));
|
||||||
|
showToast(`${err}`, ToastType.Error);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setAppLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearCurrentApp = () => (dispatch: Dispatch<any>) => {
|
||||||
|
dispatch({
|
||||||
|
type: AppActionTypes.CLEAR_APP,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,10 @@ const appsReducer = (state: any = initialAppsState, action: any) => {
|
||||||
...state,
|
...state,
|
||||||
apps: action.payload,
|
apps: action.payload,
|
||||||
};
|
};
|
||||||
case AppActionTypes.SET_APP_MODAL_LOADING:
|
case AppActionTypes.SET_APP_LOADING:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
appModalLoading: action.payload,
|
appLoading: action.payload,
|
||||||
};
|
};
|
||||||
case AppActionTypes.SET_APPS_LOADING:
|
case AppActionTypes.SET_APPS_LOADING:
|
||||||
return {
|
return {
|
||||||
|
@ -26,17 +26,16 @@ const appsReducer = (state: any = initialAppsState, action: any) => {
|
||||||
};
|
};
|
||||||
case AppActionTypes.FETCH_APP:
|
case AppActionTypes.FETCH_APP:
|
||||||
case AppActionTypes.UPDATE_APP:
|
case AppActionTypes.UPDATE_APP:
|
||||||
case AppActionTypes.CREATE_APP:
|
case AppActionTypes.INSTALL_APP:
|
||||||
case AppActionTypes.CREATE_BATCH_APPS:
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isModalVisible: false,
|
currentApp: action.payload,
|
||||||
app: action.payload,
|
|
||||||
};
|
};
|
||||||
|
case AppActionTypes.CLEAR_APP:
|
||||||
case AppActionTypes.DELETE_APP:
|
case AppActionTypes.DELETE_APP:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
app: {},
|
currentApp: {},
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { State } from 'src/redux';
|
import { State } from 'src/redux';
|
||||||
|
|
||||||
export const getApps = (state: State) => state.apps.apps;
|
export const getApps = (state: State) => state.apps.apps;
|
||||||
export const getAppById = (state: State) => state.apps.app;
|
export const getCurrentApp = (state: State) => state.apps.currentApp;
|
||||||
export const getAppslLoading = (state: State) => state.apps.appsLoading;
|
export const getAppLoading = (state: State) => state.apps.appLoading;
|
||||||
|
export const getAppsLoading = (state: State) => state.apps.appsLoading;
|
||||||
|
|
|
@ -9,17 +9,6 @@ export interface CurrentUserState extends App {
|
||||||
export interface AppsState {
|
export interface AppsState {
|
||||||
currentApp: CurrentUserState;
|
currentApp: CurrentUserState;
|
||||||
apps: App[];
|
apps: App[];
|
||||||
app: App;
|
appLoading: boolean;
|
||||||
appsLoading: boolean;
|
appsLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface CurrentUserUpdateAPI {
|
|
||||||
// id: number;
|
|
||||||
// phoneNumber?: string;
|
|
||||||
// email?: string;
|
|
||||||
// language?: string;
|
|
||||||
// country?: string;
|
|
||||||
// firstName?: string;
|
|
||||||
// lastName?: string;
|
|
||||||
// password?: string;
|
|
||||||
// }
|
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
import { App } from './types';
|
import { App, AppStatus, AppStatusEnum } from './types';
|
||||||
|
|
||||||
|
const transformAppStatus = (status: AppStatus) => {
|
||||||
|
if (status.installed && status.ready) return AppStatusEnum.Installed;
|
||||||
|
if (status.installed && !status.ready) return AppStatusEnum.Installing;
|
||||||
|
return AppStatusEnum.NotInstalled;
|
||||||
|
};
|
||||||
|
|
||||||
export const transformApp = (response: any): App => {
|
export const transformApp = (response: any): App => {
|
||||||
return {
|
return {
|
||||||
id: response.id ?? '',
|
id: response.id ?? '',
|
||||||
name: response.name ?? '',
|
name: response.name ?? '',
|
||||||
slug: response.slug ?? '',
|
slug: response.slug ?? '',
|
||||||
url: response.url ?? '',
|
status: transformAppStatus(response.status),
|
||||||
|
url: response.url,
|
||||||
|
automaticUpdates: response.automatic_updates,
|
||||||
|
assetSrc: `/assets/${response.slug}.svg`,
|
||||||
|
markdownSrc: `/markdown/${response.slug}.md`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformAppRequest = (data: App) => {
|
||||||
|
return {
|
||||||
|
automatic_updates: data.automaticUpdates,
|
||||||
|
configuration: data.configuration,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformInstallAppRequest = (data: App) => {
|
||||||
|
return {
|
||||||
|
url: data.url,
|
||||||
|
configuration: data.configuration,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,29 @@
|
||||||
export interface App {
|
export interface App {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
status?: AppStatusEnum;
|
||||||
url: string;
|
url: string;
|
||||||
|
automaticUpdates: boolean;
|
||||||
|
configuration?: string;
|
||||||
|
assetSrc?: string;
|
||||||
|
markdownSrc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisableAppForm {
|
||||||
|
slug: string;
|
||||||
|
removeAppData: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppStatus {
|
||||||
|
installed: boolean;
|
||||||
|
ready: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AppStatusEnum {
|
||||||
|
NotInstalled = 'Not installed',
|
||||||
|
Installed = 'Installed',
|
||||||
|
Installing = 'Installing',
|
||||||
|
External = 'External',
|
||||||
}
|
}
|
||||||
|
|
|
@ -1941,6 +1941,11 @@
|
||||||
jest-diff "^26.0.0"
|
jest-diff "^26.0.0"
|
||||||
pretty-format "^26.0.0"
|
pretty-format "^26.0.0"
|
||||||
|
|
||||||
|
"@types/js-yaml@^4.0.5":
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
|
||||||
|
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
|
||||||
|
|
||||||
"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
|
"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
|
||||||
version "7.0.9"
|
version "7.0.9"
|
||||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz"
|
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz"
|
||||||
|
|
Loading…
Reference in a new issue