diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c2e7cd9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.dockerignore +Dockerfile +node_modules +build +README.md diff --git a/.env.example b/.env.example deleted file mode 100644 index f3afcc4..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_API_URL=http://stackspin_proxy:8081/api/v1 -REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1a521a6 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +export NODE_OPTIONS=--openssl-legacy-provider diff --git a/.gitignore b/.gitignore index 27f16a3..f3055d4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # misc .DS_Store .env +.envrc .env.local .env.development.local .env.test.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..723bb54 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:14-alpine AS BUILDER + +WORKDIR /app +COPY package.json yarn.lock /app/ +RUN yarn install +COPY . /app +RUN yarn build + +FROM nginx:latest +COPY deployment/nginx.conf /etc/nginx/nginx.conf +COPY --from=builder /app/build /usr/share/nginx/html diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6ea880 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +build: + $(MAKE) -C backend build + docker push yksflip/dashboard-backend:latest + docker build -t dashboard . + docker tag dashboard yksflip/dashboard:latest + docker push yksflip/dashboard:latest + +rm: + docker stack rm ${STACK_NAME} + +deploy: rm + DOMAIN=${DOMAIN} docker stack deploy --resolve-image always --compose-file compose.yml ${STACK_NAME} + +update: + docker pull yksflip/dashboard:latest + docker service update dashboard_app --image yksflip/dashboard:latest --force + +exec: + docker exec -it $$(docker ps --format "{{ .Names }}" | grep dashboard) bash diff --git a/backend/.env.sample b/backend/.env.sample new file mode 100644 index 0000000..876837d --- /dev/null +++ b/backend/.env.sample @@ -0,0 +1,9 @@ +HYDRA_CLIENT_ID= +HYDRA_CLIENT_SECRET= +HYDRA_AUTHORIZATION_BASE_URL="https://sso.example.org/application/o/authorize/" +HYDRA_PUBLIC_URL="https://sso.example.org/application/o/" +TOKEN_URL="https://sso.example.org/application/o/token/" +REDIRECT_URL="https://example.org/login-callback" +SECRET_KEY= +LOAD_INCLUSTER_CONFIG=false +DATABASE_URL=sqlite:///database.db diff --git a/backend/.gitignore b/backend/.gitignore index fbc8c9d..0acc03e 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -8,3 +8,4 @@ __pycache__ .envrc .direnv run_app.local.sh +*.db diff --git a/backend/Dockerfile b/backend/Dockerfile index 03f05e3..4db00e1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -15,11 +15,11 @@ ADD requirements.txt . # pip install the local requirements.txt RUN pip install -r requirements.txt -# now copy all the files in this directory to /code +# now copy all the files in this directory to /app ADD . . # Listen to port 80 at runtime EXPOSE 5000 # Define our command to be run when launching the container -CMD ["gunicorn", "app:app", "-b", "0.0.0.0:5000", "--workers", "4", "--reload", "--capture-output", "--enable-stdio-inheritance", "--log-level", "DEBUG"] +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..7b85e16 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,14 @@ +build: + docker build -t dashboard-backend . + docker tag dashboard-backend yksflip/dashboard-backend:latest + +clean: + rm database.db + flask db upgrade + +demo: + flask cli app create nextcloud Dateiablage "https://cloud.dev.local-it.cloud" + flask cli app create vikunja Projekte "https://vikunja.dev.local-it.cloud" + +run: + flask run diff --git a/backend/areas/apps/models_lit.py b/backend/areas/apps/models_lit.py new file mode 100644 index 0000000..1132970 --- /dev/null +++ b/backend/areas/apps/models_lit.py @@ -0,0 +1,32 @@ +"""Everything to do with Apps""" + +from database import db +from .models import App + +class LITApp(App): + """ + """ + + def get_url(self): + return self.url + + def to_dict(self): + """ + represent this object as a dict, compatible for JSON output + """ + return {"id": self.id, + "name": self.name, + "slug": self.slug, + "external": self.external, + "status": self.get_status(), + "url": self.get_url()} + + + + def get_status(self): + """Returns an AppStatus object that describes the current cluster state""" + return { + "installed": "", + "ready": "", + "message": "", + } diff --git a/backend/areas/auth/__init__.py b/backend/areas/auth/__init__.py index d7c8ad3..d545fc1 100644 --- a/backend/areas/auth/__init__.py +++ b/backend/areas/auth/__init__.py @@ -1 +1 @@ -from .auth import * \ No newline at end of file +from .lit_auth import * \ No newline at end of file diff --git a/backend/areas/auth/auth.py b/backend/areas/auth/auth.py index c972752..4e96088 100644 --- a/backend/areas/auth/auth.py +++ b/backend/areas/auth/auth.py @@ -30,38 +30,39 @@ def hydra_callback(): token = HydraOauth.get_token(state, code) user_info = HydraOauth.get_user_info() # Match Kratos identity with Hydra - identities = KratosApi.get("/identities") - identity = None - for i in identities.json(): - if i["traits"]["email"] == user_info["email"]: - identity = i + # identities = KratosApi.get("/identities") + # identity = None + # for i in identities.json(): + # if i["traits"]["email"] == user_info["email"]: + # identity = i access_token = create_access_token( - identity=token, expires_delta=timedelta(days=365), additional_claims={"user_id": identity["id"]} + identity=token, expires_delta=timedelta(days=365), + #additional_claims={"user_id": identity["id"]} ) - apps = App.query.all() - app_roles = [] - for app in apps: - tmp_app_role = AppRole.query.filter_by( - user_id=identity["id"], app_id=app.id - ).first() - app_roles.append( - { - "name": app.slug, - "role_id": tmp_app_role.role_id if tmp_app_role else None, - } - ) + # apps = App.query.all() + # app_roles = [] + # for app in apps: + # tmp_app_role = AppRole.query.filter_by( + # user_id=identity["id"], app_id=app.id + # ).first() + # app_roles.append( + # { + # "name": app.slug, + # "role_id": tmp_app_role.role_id if tmp_app_role else None, + # } + # ) return jsonify( { "accessToken": access_token, "userInfo": { - "id": identity["id"], + "id": user_info["email"], "email": user_info["email"], "name": user_info["name"], "preferredUsername": user_info["preferred_username"], - "app_roles": app_roles, + # "app_roles": app_roles, }, } ) diff --git a/backend/areas/auth/lit_auth.py b/backend/areas/auth/lit_auth.py new file mode 100644 index 0000000..f804690 --- /dev/null +++ b/backend/areas/auth/lit_auth.py @@ -0,0 +1,63 @@ +from multiprocessing import current_process +from flask import jsonify, request +from flask_jwt_extended import create_access_token +from flask_cors import cross_origin +from datetime import timedelta + +from areas import api_v1 +from areas.apps import App, AppRole +from config import * +from helpers import HydraOauth, BadRequest + + +@api_v1.route("/login", methods=["POST"]) +@cross_origin() +def login(): + authorization_url = HydraOauth.authorize() + return jsonify({"authorizationUrl": authorization_url}) + + +@api_v1.route("/hydra/callback") +@cross_origin() +def hydra_callback(): + state = request.args.get("state") + code = request.args.get("code") + if state == None: + raise BadRequest("Missing state query param") + + if code == None: + raise BadRequest("Missing code query param") + + token = HydraOauth.get_token(state, code) + user_info = HydraOauth.get_user_info() + + access_token = create_access_token( + identity=token, expires_delta=timedelta(days=365), + #additional_claims={"user_id": identity["id"]} + ) + + # apps = App.query.all() + # app_roles = [] + # for app in apps: + # tmp_app_role = AppRole.query.filter_by( + # user_id=identity["id"], app_id=app.id + # ).first() + # app_roles.append( + # { + # "name": app.slug, + # "role_id": tmp_app_role.role_id if tmp_app_role else None, + # } + # ) + + return jsonify( + { + "accessToken": access_token, + "userInfo": { + "id": user_info["email"], + "email": user_info["email"], + "name": user_info["name"], + "preferredUsername": user_info["preferred_username"], + # "app_roles": app_roles, + }, + } + ) diff --git a/backend/config.py b/backend/config.py index 2cb0017..15874a9 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,10 +1,19 @@ import os -SECRET_KEY = os.environ.get("SECRET_KEY") +def env_file(key: str): + file_env = os.environ.get(f"{key}_FILE") + if file_env and os.path.exists(file_env): + return open(file_env).read().rstrip('\n') + return os.environ.get(key) + +SECRET_KEY = env_file("SECRET_KEY") + HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID") -HYDRA_CLIENT_SECRET = os.environ.get("HYDRA_CLIENT_SECRET") +HYDRA_CLIENT_SECRET = env_file("HYDRA_CLIENT_SECRET") + HYDRA_AUTHORIZATION_BASE_URL = os.environ.get("HYDRA_AUTHORIZATION_BASE_URL") TOKEN_URL = os.environ.get("TOKEN_URL") +REDIRECT_URL = os.environ.get("REDIRECT_URL") LOGIN_PANEL_URL = os.environ.get("LOGIN_PANEL_URL") diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100755 index 0000000..f5a05a4 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -eu +env +flask db upgrade +gunicorn app:app -b 0.0.0.0:5000 --workers "$(nproc)" --reload --capture-output --enable-stdio-inheritance --log-level DEBUG \ No newline at end of file diff --git a/backend/helpers/hydra_oauth.py b/backend/helpers/hydra_oauth.py index b75d615..5143487 100644 --- a/backend/helpers/hydra_oauth.py +++ b/backend/helpers/hydra_oauth.py @@ -9,7 +9,7 @@ class HydraOauth: @staticmethod def authorize(): try: - hydra = OAuth2Session(HYDRA_CLIENT_ID) + hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL) authorization_url, state = hydra.authorization_url( HYDRA_AUTHORIZATION_BASE_URL ) diff --git a/backend/helpers/kubernetes.py b/backend/helpers/kubernetes.py index cba600e..6680ec0 100644 --- a/backend/helpers/kubernetes.py +++ b/backend/helpers/kubernetes.py @@ -20,11 +20,11 @@ from config import LOAD_INCLUSTER_CONFIG # # By default this loads whatever we define in the `KUBECONFIG` env variable, # otherwise loads the config from default locations, similar to what kubectl -# does. -if LOAD_INCLUSTER_CONFIG: - config.load_incluster_config() -else: - config.load_kube_config() +# # does. +# if LOAD_INCLUSTER_CONFIG: +# config.load_incluster_config() +# else: +# config.load_kube_config() def create_variables_secret(app_slug, variables_filepath): """Checks if a variables secret for app_name already exists, generates it if necessary. diff --git a/backend/migrations/versions/27761560bbcb_.py b/backend/migrations/versions/27761560bbcb_.py deleted file mode 100644 index baa80e4..0000000 --- a/backend/migrations/versions/27761560bbcb_.py +++ /dev/null @@ -1,46 +0,0 @@ -"""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 ### diff --git a/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py b/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py deleted file mode 100644 index 5caae97..0000000 --- a/backend/migrations/versions/3fa0c38ea1ac_add_velero_as_app.py +++ /dev/null @@ -1,25 +0,0 @@ -"""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 diff --git a/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py b/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py deleted file mode 100644 index 53a8a1d..0000000 --- a/backend/migrations/versions/5f462d2d9d25_convert_role_column_to_table.py +++ /dev/null @@ -1,48 +0,0 @@ -"""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 ### diff --git a/backend/migrations/versions/b514cca2d47b_add_user_role.py b/backend/migrations/versions/b514cca2d47b_add_user_role.py deleted file mode 100644 index 0586942..0000000 --- a/backend/migrations/versions/b514cca2d47b_add_user_role.py +++ /dev/null @@ -1,76 +0,0 @@ -"""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") diff --git a/backend/migrations/versions/d70b750a1297_.py b/backend/migrations/versions/d70b750a1297_.py new file mode 100644 index 0000000..cc5d6a0 --- /dev/null +++ b/backend/migrations/versions/d70b750a1297_.py @@ -0,0 +1,51 @@ +"""empty message + +Revision ID: d70b750a1297 +Revises: +Create Date: 2022-10-25 11:32:27.303354 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd70b750a1297' +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.Column('external', sa.Boolean(), server_default='0', nullable=False), + sa.Column('url', sa.String(length=128), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug') + ) + op.create_table('role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + 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_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['app_id'], ['app.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['role.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('role') + op.drop_table('app') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/e08df0bef76f_.py b/backend/migrations/versions/e08df0bef76f_.py deleted file mode 100644 index 005833f..0000000 --- a/backend/migrations/versions/e08df0bef76f_.py +++ /dev/null @@ -1,33 +0,0 @@ -"""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 ### diff --git a/deployment/Dockerfile b/deployment/Dockerfile deleted file mode 100644 index bddffa6..0000000 --- a/deployment/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM nginx:latest -COPY ./nginx.conf /etc/nginx/nginx.conf -COPY . /usr/share/nginx/html diff --git a/package.json b/package.json index 5739c7e..8b57724 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.22.0", "react-hot-toast": "^2.0.0", + "react-iframe": "^1.8.0", "react-markdown": "^7.0.1", "react-redux": "^7.2.4", "react-router": "6.2.1", diff --git a/public/assets/lit_logos/apple-touch-icon_lit_transp.png b/public/assets/lit_logos/apple-touch-icon_lit_transp.png new file mode 100644 index 0000000..8a721f2 Binary files /dev/null and b/public/assets/lit_logos/apple-touch-icon_lit_transp.png differ diff --git a/public/assets/lit_logos/favicon_lit_transp.ico b/public/assets/lit_logos/favicon_lit_transp.ico new file mode 100644 index 0000000..5033fd3 Binary files /dev/null and b/public/assets/lit_logos/favicon_lit_transp.ico differ diff --git a/public/assets/lit_logos/lit_transp.png b/public/assets/lit_logos/lit_transp.png new file mode 100644 index 0000000..f9a1dfb Binary files /dev/null and b/public/assets/lit_logos/lit_transp.png differ diff --git a/public/assets/lit_logos/lit_transp_16x16.png b/public/assets/lit_logos/lit_transp_16x16.png new file mode 100644 index 0000000..663e3e6 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_16x16.png differ diff --git a/public/assets/lit_logos/lit_transp_192x192.png b/public/assets/lit_logos/lit_transp_192x192.png new file mode 100644 index 0000000..9ec7fae Binary files /dev/null and b/public/assets/lit_logos/lit_transp_192x192.png differ diff --git a/public/assets/lit_logos/lit_transp_256x256.png b/public/assets/lit_logos/lit_transp_256x256.png new file mode 100644 index 0000000..2b9f0e3 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_256x256.png differ diff --git a/public/assets/lit_logos/lit_transp_32x32.png b/public/assets/lit_logos/lit_transp_32x32.png new file mode 100644 index 0000000..55b681d Binary files /dev/null and b/public/assets/lit_logos/lit_transp_32x32.png differ diff --git a/public/assets/lit_logos/lit_transp_512x512.png b/public/assets/lit_logos/lit_transp_512x512.png new file mode 100644 index 0000000..8c5481e Binary files /dev/null and b/public/assets/lit_logos/lit_transp_512x512.png differ diff --git a/public/assets/lit_logos/lit_transp_title.png b/public/assets/lit_logos/lit_transp_title.png new file mode 100644 index 0000000..5715060 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title.png differ diff --git a/public/assets/lit_logos/lit_transp_title_52.png b/public/assets/lit_logos/lit_transp_title_52.png new file mode 100644 index 0000000..34c0400 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title_52.png differ diff --git a/public/assets/lit_logos/lit_transp_title_96.png b/public/assets/lit_logos/lit_transp_title_96.png new file mode 100644 index 0000000..f535bbc Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title_96.png differ diff --git a/public/assets/lit_logos/lit_transp_title_sub.png b/public/assets/lit_logos/lit_transp_title_sub.png new file mode 100644 index 0000000..e4c3444 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title_sub.png differ diff --git a/public/assets/lit_logos/lit_transp_title_sub_192.png b/public/assets/lit_logos/lit_transp_title_sub_192.png new file mode 100644 index 0000000..93a5d4c Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title_sub_192.png differ diff --git a/public/assets/lit_logos/lit_transp_title_sub_96.png b/public/assets/lit_logos/lit_transp_title_sub_96.png new file mode 100644 index 0000000..e1b27c3 Binary files /dev/null and b/public/assets/lit_logos/lit_transp_title_sub_96.png differ diff --git a/public/assets/logo-small.svg b/public/assets/logo-small.svg deleted file mode 100644 index 3b378bb..0000000 --- a/public/assets/logo-small.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/public/assets/logo.svg b/public/assets/logo.svg deleted file mode 100644 index e33262f..0000000 --- a/public/assets/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/public/assets/user.svg b/public/assets/user.svg new file mode 100644 index 0000000..9f069e0 --- /dev/null +++ b/public/assets/user.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/public/assets/vikunja.svg b/public/assets/vikunja.svg new file mode 100644 index 0000000..0d337aa --- /dev/null +++ b/public/assets/vikunja.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/env.js b/public/env.js new file mode 100644 index 0000000..d47bfbb --- /dev/null +++ b/public/env.js @@ -0,0 +1,3 @@ +window.env = { + REACT_APP_API_URL: 'http://localhost:5000/api/v1', +}; diff --git a/public/index.html b/public/index.html index aa069f2..1d286f3 100644 --- a/public/index.html +++ b/public/index.html @@ -1,21 +1,19 @@ - - - - - - - - - - - React App - - - -
- - - + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..f50a0ad 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,17 +3,17 @@ "name": "Create React App Sample", "icons": [ { - "src": "favicon.ico", + "src": "assets/lit_logos/favicon_lit_transp.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "assets/lit_logos/lit_transp_192x192.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "assets/lit_logos/lit_transp_512x512.png", "type": "image/png", "sizes": "512x512" } diff --git a/src/App.tsx b/src/App.tsx index 7d2d7e1..83bb6f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,43 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Helmet } from 'react-helmet'; -import { Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { Toaster } from 'react-hot-toast'; +import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { useAuth } from 'src/services/auth'; -import { Dashboard, Users, Login, Apps, AppSingle } from './modules'; +import { Dashboard } from './modules'; import { Layout } from './components'; +import { AppIframe } from './modules/dashboard/AppIframe'; import { LoginCallback } from './modules/login/LoginCallback'; +import { useApps } from './services/apps'; +import { Login } from './modules/login'; +import { Users } from './modules/users/Users'; +import { AppSingle } from './modules/apps/AppSingle'; +import { Apps } from './modules/apps/Apps'; +import { DashboardLIT } from './modules/dashboard/DashboardLIT'; // eslint-disable-next-line @typescript-eslint/no-unused-vars function App() { const { authToken, currentUser, isAdmin } = useAuth(); - const redirectToLogin = !authToken || !currentUser?.app_roles; const ProtectedRoute = () => { return isAdmin ? : ; }; + const { apps, loadApps } = useApps(); + + useEffect(() => { + loadApps(); + }, []); + return ( <> - Stackspin + Dashboard - - - + + + @@ -41,7 +53,10 @@ function App() { ) : ( - } /> + } /> + {apps.map((app) => ( + } /> + ))} }> } /> diff --git a/src/components/Header/HeaderLIT.tsx b/src/components/Header/HeaderLIT.tsx new file mode 100644 index 0000000..d385158 --- /dev/null +++ b/src/components/Header/HeaderLIT.tsx @@ -0,0 +1,178 @@ +import React, { Fragment, useMemo, useState } from 'react'; +import { Disclosure, Menu, Transition } from '@headlessui/react'; +import { MenuIcon, XIcon } from '@heroicons/react/outline'; +import { useAuth } from 'src/services/auth'; +import Gravatar from 'react-gravatar'; +import { Link, useLocation } from 'react-router-dom'; +import clsx from 'clsx'; +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 HeaderLIT: React.FC = () => { + const [currentUserModal, setCurrentUserModal] = useState(false); + const [currentUserId, setCurrentUserId] = useState(null); + const { logOut, currentUser, isAdmin } = useAuth(); + + const { pathname } = useLocation(); + 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 ( + <> + + {({ open }) => ( +
+
+
+
+ {/* Mobile menu button */} + + Open main menu + {open ? ( + +
+
+ + Local-IT + Local-IT + +
+ {/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */} + {apps.map((app) => ( + + {app.name} + + ))} +
+
+ +
+
+ + +
+ {apps.map((app) => ( + + {app.name} + + ))} +
+
+
+ )} +
+ + {currentUserModal && ( + + )} + + ); +}; + +export default HeaderLIT; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts index 5653319..527f03a 100644 --- a/src/components/Header/index.ts +++ b/src/components/Header/index.ts @@ -1 +1,2 @@ export { default as Header } from './Header'; +export { default as HeaderLIT } from './HeaderLIT'; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index ab60d93..9467e21 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Header } from '../Header'; +import { HeaderLIT } from '../Header'; const Layout: React.FC = ({ children }) => { return ( <> -
+ {children} diff --git a/src/components/index.ts b/src/components/index.ts index 9a2f607..7c60c55 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,7 @@ export { Layout } from './Layout'; -export { Header } from './Header'; +export { Header, HeaderLIT } from './Header'; export { Table } from './Table'; export { Banner } from './Banner'; export { Tabs } from './Tabs'; export { Modal, ConfirmationModal, StepsModal } from './Modal'; -export { UserModal } from './UserModal'; export { ProgressSteps } from './ProgressSteps'; diff --git a/src/index.css b/src/index.css index 28b49ee..d93aadd 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; +@import "./lit_navigation_style.css"; div[tabindex] { flex: 1; diff --git a/src/lit_navigation_style.css b/src/lit_navigation_style.css new file mode 100644 index 0000000..c7c8dca --- /dev/null +++ b/src/lit_navigation_style.css @@ -0,0 +1,57 @@ +.litbutton{ + color: #755d86; + text-transform: uppercase; +} +.litbutton-card{ + color: #000000; +} + +.litbutton-card:before, +.litbutton:before { + content: "["; + display: inline-block; + opacity: 0; + -webkit-transform: translateX(20px); + -moz-transform: translateX(20px); + transform: translateX(20px); + -webkit-transition: -webkit-transform 0.3s, opacity 0.2s; + -moz-transition: -moz-transform 0.3s, opacity 0.2s; + transition: transform 0.3s, opacity 0.2s; +} +.litbutton:before{ + margin-right: 10px; +} +.litbutton-card:after, +.litbutton:after { + content: "]"; + display: inline-block; + opacity: 0; + -webkit-transition: -webkit-transform 0.3s, opacity 0.2s; + -moz-transition: -moz-transform 0.3s, opacity 0.2s; + transition: transform 0.3s, opacity 0.2s; + -webkit-transform: translateX(-20px); + -moz-transform: translateX(-20px); + transform: translateX(-20px); +} +.litbutton:after{ + margin-left: 10px; + +} +.litbutton-card:hover, +.litbutton:hover{ + color: #3a97a3; +} +.litbutton-card:active, +.litbutton-active{ + color: #3a97a3; +} +.litbutton-card:hover:before, +.litbutton-card:hover:after, +.litbutton:hover:before, +.litbutton:hover:after { + color: #3a97a3; + opacity: 1; + -webkit-transform: translateX(0px); + -moz-transform: translateX(0px); + transform: translateX(0px); +} diff --git a/src/modules/dashboard/AppIframe.tsx b/src/modules/dashboard/AppIframe.tsx new file mode 100644 index 0000000..dc19b9d --- /dev/null +++ b/src/modules/dashboard/AppIframe.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Iframe from 'react-iframe'; + +export const AppIframe: React.FC = ({ app }: { app: any }) => { + return ( +
+
+