├── .envTest-sample ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── docker-test.yml ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── Change.log.fr.md ├── Change.log.md ├── CoverageExplanations.md ├── Dockerfile ├── Dockerfile-dev ├── LICENSE ├── README-fr.md ├── README.md ├── build-dev.sh ├── build-prod.sh ├── cacerts └── .keep ├── checkHardCoded.sh ├── client ├── .eslintrc.cjs ├── .gitignore ├── .storybook │ ├── main.ts │ └── preview.tsx ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ ├── MytinyDC_logo-brand-full-darkmode.png │ │ ├── MytinyDC_logo-brand-full.png │ │ └── download_image_1700775996997.png │ └── favicon.ico ├── src │ ├── App.tsx │ ├── api │ │ └── mytinydcUPDONApi.ts │ ├── app │ │ ├── Router.tsx │ │ ├── contextSlice.ts │ │ ├── css │ │ │ ├── loadership.scss │ │ │ └── root.scss │ │ ├── hook.ts │ │ ├── serviceMessageSlice.ts │ │ └── store.ts │ ├── components │ │ ├── ActionsSettings.scss │ │ ├── ActionsSettings.stories.tsx │ │ ├── ActionsSettings.tsx │ │ ├── Badge.scss │ │ ├── Badge.stories.tsx │ │ ├── Badge.tsx │ │ ├── Block.scss │ │ ├── Block.stories.tsx │ │ ├── Block.tsx │ │ ├── ButtonGeneric.scss │ │ ├── ButtonGeneric.stories.tsx │ │ ├── ButtonGeneric.tsx │ │ ├── CheckBox.scss │ │ ├── CheckBox.stories.tsx │ │ ├── CheckBox.tsx │ │ ├── ConfirmDialog.scss │ │ ├── ConfirmDialog.stories.tsx │ │ ├── ConfirmDialog.tsx │ │ ├── Control.scss │ │ ├── Control.stories.tsx │ │ ├── Control.tsx │ │ ├── ControlGroupButtons.scss │ │ ├── ControlGroupButtons.stories.tsx │ │ ├── ControlGroupButtons.tsx │ │ ├── Dialog.scss │ │ ├── Dialog.stories.tsx │ │ ├── Dialog.tsx │ │ ├── DisplayVersions.scss │ │ ├── DisplayVersions.stories.tsx │ │ ├── DisplayVersions.tsx │ │ ├── FieldSet.scss │ │ ├── FieldSet.stories.tsx │ │ ├── FieldSet.tsx │ │ ├── FieldSetApiEntrypoint.scss │ │ ├── FieldSetApiEntrypoint.stories.tsx │ │ ├── FieldSetApiEntrypoint.tsx │ │ ├── FieldSetAuthorizationHeader.scss │ │ ├── FieldSetAuthorizationHeader.stories.tsx │ │ ├── FieldSetAuthorizationHeader.tsx │ │ ├── FieldSetClickableUrl.scss │ │ ├── FieldSetClickableUrl.stories.tsx │ │ ├── FieldSetClickableUrl.tsx │ │ ├── GlobalGithubToken.scss │ │ ├── GlobalGithubToken.stories.tsx │ │ ├── GlobalGithubToken.tsx │ │ ├── Header.scss │ │ ├── Header.stories.tsx │ │ ├── Header.tsx │ │ ├── HttpHeader.scss │ │ ├── HttpHeader.stories.tsx │ │ ├── HttpHeader.tsx │ │ ├── IconWithTooltip.scss │ │ ├── IconWithTooltip.stories.tsx │ │ ├── IconWithTooltip.tsx │ │ ├── ImageUploader.scss │ │ ├── ImageUploader.stories.tsx │ │ ├── ImageUploader.tsx │ │ ├── InputGeneric.scss │ │ ├── InputGeneric.stories.tsx │ │ ├── InputGeneric.tsx │ │ ├── InputIcon.scss │ │ ├── InputIcon.stories.tsx │ │ ├── InputIcon.tsx │ │ ├── LoginBlock.scss │ │ ├── LoginBlock.stories.tsx │ │ ├── LoginBlock.tsx │ │ ├── ResultCompare.scss │ │ ├── ResultCompare.stories.tsx │ │ ├── ResultCompare.tsx │ │ ├── ScrapGitHubReleaseTags.scss │ │ ├── ScrapGitHubReleaseTags.stories.tsx │ │ ├── ScrapGitHubReleaseTags.tsx │ │ ├── ScrapProduction.scss │ │ ├── ScrapProduction.stories.tsx │ │ ├── ScrapProduction.tsx │ │ ├── Search.scss │ │ ├── Search.stories.tsx │ │ ├── Search.tsx │ │ ├── SelectGeneric.scss │ │ ├── SelectGeneric.stories.tsx │ │ ├── SelectGeneric.tsx │ │ ├── ServiceMessage.scss │ │ ├── ServiceMessage.tsx │ │ ├── Stepper.scss │ │ ├── Stepper.stories.tsx │ │ ├── Stepper.tsx │ │ ├── StepperStep.scss │ │ ├── StepperStep.stories.tsx │ │ ├── StepperStep.tsx │ │ ├── Summary.scss │ │ ├── Summary.stories.tsx │ │ ├── Summary.tsx │ │ ├── Toast.scss │ │ ├── Toast.stories.tsx │ │ ├── Toast.tsx │ │ ├── UrlLinkButtons.scss │ │ ├── UrlLinkButtons.stories.tsx │ │ ├── UrlLinkButtons.tsx │ │ ├── UrlOpener.scss │ │ ├── UrlOpener.stories.tsx │ │ └── UrlOpener.tsx │ ├── features │ │ ├── changepassword │ │ │ ├── ChangePassword.scss │ │ │ ├── ChangePassword.stories.tsx │ │ │ └── ChangePassword.tsx │ │ ├── controlmanagement │ │ │ ├── ControlManager.scss │ │ │ ├── ControlManager.stories.ts │ │ │ └── ControlManager.tsx │ │ ├── curlcommands │ │ │ ├── CurlCommands.scss │ │ │ ├── CurlCommands.stories.tsx │ │ │ └── CurlCommands.tsx │ │ ├── displaycontrols │ │ │ ├── DisplayControls.scss │ │ │ └── DisplayControls.tsx │ │ ├── errors │ │ │ ├── ErrorInRouter.scss │ │ │ ├── ErrorInRouter.stories.tsx │ │ │ └── ErrorInRouter.tsx │ │ ├── homepage │ │ │ ├── PageHome.scss │ │ │ └── PageHome.tsx │ │ ├── login │ │ │ ├── PageLogin.scss │ │ │ ├── PageLogin.stories.tsx │ │ │ └── PageLogin.tsx │ │ └── usermanager │ │ │ ├── UserManager.scss │ │ │ ├── UserManager.stories.tsx │ │ │ └── UserManager.tsx │ ├── helpers │ │ ├── DateHelper.ts │ │ ├── ExprSamples.ts │ │ ├── UiMiscHelper.ts │ │ ├── rtk.ts │ │ └── scrapUrl.ts │ ├── main.tsx │ └── vite-env.d.ts ├── tools │ ├── addComponents.sh │ └── tplComponents │ │ ├── TPL.scss │ │ ├── TPL.stories.tsx │ │ └── TPL.tsx ├── tsconfig.json └── vite.config.ts ├── data └── .gitkeep ├── doc ├── CONTROL.md ├── GROUPS.md ├── INSTALL.md ├── assets │ ├── Screenshot_addGroup.png │ ├── Screenshot_setGroupsControl.png │ ├── Screenshot_usersgroupsmanager.png │ └── utdon-dashboard-mytinydc.com.png └── en │ ├── CONTROL.md │ ├── GROUPS.md │ └── INSTALL.md ├── install-legacy.sh ├── jest.config.ts ├── jestLoadEnvironmentTest.ts ├── locales └── fr.json ├── openapi.yaml ├── package-lock.json ├── package.json ├── src ├── Constants-dev.ts ├── Constants.ts ├── Global.types.ts ├── ServerTypes.ts ├── lib │ ├── Authentification.ts │ ├── Database.ts │ ├── Features.ts │ ├── GlobalGithubToken.ts │ ├── PatchVersion.ts │ ├── helperGitRepository.ts │ ├── helperProdVersionReader.ts │ ├── logs.ts │ └── scrapUrlServer.ts ├── main.ts └── routes │ ├── routerActions.ts │ ├── routerAuth.ts │ ├── routerControls.ts │ └── routerCore.ts ├── test ├── Authentification.test.ts ├── Database.test.ts ├── Features.test.ts ├── Groups.test.ts ├── cacerts │ └── .keep ├── checkJestInstallation │ ├── sum.test.ts │ └── sum.ts ├── data │ └── .gitkeep ├── helperGitRepository.test.ts ├── helperProdVersionReader.test.ts ├── samples │ ├── database-empty.json │ ├── database-malformed-object.json │ ├── database-nocontent.json │ ├── database-readonly.json │ ├── database.json │ ├── html-version.response.html │ ├── json-version.response.json │ ├── reorder1.json │ ├── reorder2.json │ ├── tags-reponse-github-tags.json │ └── users-before-PR#15.json └── scrapUrlServer.test.ts ├── tsconfig.json └── updateVersion.sh /.envTest-sample: -------------------------------------------------------------------------------- 1 | # No ssl proxy - proxy use CONNECT method 2 | 3 | HTTP_PROXY="http://192.168.1.1:8080" 4 | HTTPS_PROXY="http://192.168.1.1:8080" 5 | 6 | # With ssl proxy CA certificate must be set && url proxy must start with https 7 | 8 | # put ca cert in tests/cacerts/proxy.ca 9 | 10 | # value is adjusted to the production working directory 11 | 12 | PROXYCA_CERT="../test/cacerts/proxy.ca" 13 | INTSSLHTTPS_PROXY="https://proxy.mydomain:8083" 14 | GITHUBTOKEN="[your GitHub token]" 15 | 16 | HARCODEDGREP="[Words to search for] 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /client/** 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 4 | parser: "@typescript-eslint/parser", 5 | plugins: ["@typescript-eslint"], 6 | root: true, 7 | env: { 8 | browser: true, 9 | node: true, 10 | }, 11 | rules: {}, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/docker-test.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag utdon:1.0.0 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/** 2 | !data/.gitkeep 3 | dist/** 4 | node_modules 5 | test/data/** 6 | !test/data/.gitkeep 7 | test/devtest.test.ts 8 | coverage/** 9 | .envlocaldev 10 | public 11 | src/data 12 | .envTest 13 | test/cacerts/proxy.ca 14 | cacerts/proxy.ca 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/.prettierignore -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.keystyle": "flat" 3 | } 4 | -------------------------------------------------------------------------------- /Change.log.fr.md: -------------------------------------------------------------------------------- 1 | [English Changelog](./Change.log.md) 2 | 3 | # Changelogs 4 | 5 | # 1.9.0 6 | 7 | - NodeJS 20.18 8 | - Support proxy Corporate. 9 | - Amélioration de la sécurité SSL, lié à l'implémentation du support d'un proxy corporate. 10 | - **BREAKING CHANGE** : Un nouveau volume a été ajouté : "cacerts", monté sur "/app/cacerts". 11 | - **BREAKING CHANGE** : Si vous surveillez des services HTTPS avec des certificats autosignés, vous devez installer les certificats CA dans le répertoire "cacerts" ou bien désactiver le contrôle SSL en passant la variable d'environnement : `NODE_TLS_REJECT_UNAUTHORIZED="0"` 12 | - Amélioration des tests unitaires. 13 | - Typo. 14 | 15 | # 1.7.0 16 | 17 | - **BREAKING CHANGE** : Changement de la méthode HTTP pour l'entrée API : "compare". La méthode originale n'était pas appropriée, car la fonction appelée altère les données, par conséquent, elle a été remplacée par "PUT". Si vous utilisez utdon dans une tâche "cron" avec curl, ajouter le paramètre : '-X PUT' 18 | - **BREAKING CHANGE** : Harmonisation et amélioration des logs serveur, **le contenu des logs a changé**. 19 | - Refactorisation login/logout, le login retourne un nouveau cookie (corrige session fixation). 20 | - Correction de plusieurs bugs et refactorisation de méthodes. 21 | - Rechercher par uuid ou partie d'uuid. 22 | - UserManager : Le champs "username" est inactif en mode "Édition". 23 | - Présentation des contrôles sous la forme d'un tableau. 24 | - Duplication d'un contrôle. 25 | - Support des dépôts git de type "Gitea" avec authentification, permet ainsi l'authentification GitHub pour les projets privés, valeur (HTTP HEADER) Key: Authorization value: "Bearer " 26 | - Authentification GitHub globale pour supprimer la barrière "rate-limit". La valeur est prise seulement dans le cas où le contrôle ne dispose pas déjà d'une authentification spécifique. 27 | - Pour les applications n'offrant pas de point d'entrée de niveau de version, possibilité de saisir la valeur de la version utilisée, ceci peut aussi permettre de suivre l'évolution d'une application qui n'est pas en production. 28 | -------------------------------------------------------------------------------- /Change.log.md: -------------------------------------------------------------------------------- 1 | [Changelog en Français](./Change.log.fr.md) 2 | 3 | # Changelogs 4 | 5 | # 1.9.0 6 | 7 | - NodeJS 20.18 8 | - Corporate proxy support. 9 | - Improved SSL security, linked to the implementation of corporate proxy support. 10 | - **BREAKING CHANGE**: A new volume has been added: “cacerts”, mounted on “/app/cacerts”. 11 | - **BREAKING CHANGE**: If you're monitoring HTTPS services with self-signed certificates, you must install CA certificates in the “cacerts” directory or disable SSL control by passing the environment variable: `NODE_TLS_REJECT_UNAUTHORIZED=“0”`. 12 | - Improved unit testing. 13 | - Typo. 14 | 15 | # 1.7.0 16 | 17 | - **BREAKING CHANGE**: Changed the HTTP method for API input to "compare". The original method was not appropriate, as the function called alters the data, so it has been replaced by "PUT". If you use utdon in a "cron" task with curl, add the parameter: '-X PUT'. 18 | - **BREAKING CHANGE**: Harmonization and improvement of server logs, **log content has changed**. 19 | - Refactor login/logout, login returns a new cookie (fix session fixation). 20 | - Several bugs fixed and methods refactored. 21 | - Search by uuid or part of uuid. 22 | - UserManager: The username field is inactive in "Edit" mode. 23 | - Presentation of controls as table. 24 | - Control duplication. 25 | - Support for "Gitea" git repositories with authentication, enabling GitHub authentication for private projects, value (HTTP HEADER) Key: Authorization value: "Bearer ". 26 | - Global GitHub authentication to remove the "rate-limit" barrier. The value is taken only if the control does not already have a specific authentication. 27 | - For applications that don't offer a version level entry point, it is possible to enter the value of the version in use. This can also be used to track the evolution of an application that is not in production. 28 | -------------------------------------------------------------------------------- /CoverageExplanations.md: -------------------------------------------------------------------------------- 1 | ```txt 2 | -----------------------------|---------|----------|---------|---------|------------------- 3 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 4 | -----------------------------|---------|----------|---------|---------|------------------- 5 | All files | 99.13 | 96.17 | 100 | 99.8 | 6 | src | 100 | 100 | 100 | 100 | 7 | Constants.ts | 100 | 100 | 100 | 100 | 8 | src/lib | 99.07 | 96.17 | 100 | 99.79 | 9 | Authentification.ts | 97.88 | 92.39 | 100 | 99.53 | 342 => Hard (No added value) 10 | Database.ts | 100 | 96.77 | 100 | 100 | 207 => will disappear in the future 11 | Features.ts | 100 | 100 | 100 | 100 | 12 | helperGitRepository.ts | 100 | 100 | 100 | 100 | 13 | helperProdVersionReader.ts | 100 | 100 | 100 | 100 | 14 | scrapUrlServer.ts | 100 | 100 | 100 | 100 | 15 | test/checkJestInstallation | 100 | 100 | 100 | 100 | 16 | sum.ts | 100 | 100 | 100 | 100 | 17 | -----------------------------|---------|----------|---------|---------|------------------- 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # @author DHENRY for mytinydc.com 2 | # @license AGPL3 3 | 4 | ARG RUNASUSER="utdon" 5 | ARG RUNASUSERID="1001" 6 | ARG RUNASGROUP="1001" 7 | 8 | FROM node:20.18-alpine3.20 AS base 9 | 10 | # build 11 | FROM base AS builder 12 | 13 | WORKDIR /app 14 | 15 | # Server 16 | COPY ./src/ ./src/ 17 | COPY ./openapi.yaml . 18 | COPY ./package.json . 19 | COPY ./locales ./locales 20 | COPY ./tsconfig.json . 21 | # Building server, final dest is /dist 22 | RUN npm install && npm run build 23 | RUN rm -rf node_modules && npm install --omit=dev 24 | # Client 25 | COPY ./client ./client 26 | RUN rm -rf client/node_modules client/tools client/dist client/.storybook 27 | # Remove stories 28 | RUN find ./client -name "*.stories.*" -exec rm -rf {} \; 29 | 30 | # Building client, final dest is client/dist 31 | RUN cd client && npm install --omit=dev && npm run build 32 | 33 | FROM base AS runner 34 | LABEL org.opencontainers.image.source=https://github.com/dhenry123/utdon 35 | LABEL org.opencontainers.image.description="Multi arch image" 36 | LABEL org.opencontainers.image.licenses=AGPLV3 37 | 38 | ARG RUNASUSER 39 | ARG RUNASUSERID 40 | ARG RUNASGROUP 41 | 42 | # Creating user & group 43 | RUN addgroup -S ${RUNASUSER} --gid "${RUNASGROUP}" && adduser -S ${RUNASUSER} -s /bin/sh --uid "${RUNASUSERID}" -G ${RUNASUSER} 44 | 45 | USER ${RUNASUSERID} 46 | 47 | WORKDIR /app 48 | 49 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/dist/ ./ 50 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/openapi.yaml ./ 51 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/node_modules/ ./node_modules 52 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/client/dist/ ./public 53 | 54 | # data directory for mount point 55 | RUN mkdir data 56 | 57 | EXPOSE 3015 58 | 59 | CMD ["node","main.js"] 60 | -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | # @author DHENRY for mytinydc.com 2 | # @license AGPL3 3 | 4 | ARG RUNASUSER="utdon" 5 | ARG RUNASUSERID="1001" 6 | ARG RUNASGROUP="1001" 7 | 8 | FROM node:20.18-alpine3.20 AS base 9 | 10 | # build 11 | FROM base AS builder 12 | 13 | WORKDIR /app 14 | 15 | # Server 16 | COPY ./src/ ./src/ 17 | RUN rm -f ./genSwaggerJson.ts 18 | COPY ./openapi.yaml . 19 | COPY ./package.json . 20 | COPY ./locales ./locales 21 | COPY ./tsconfig.json . 22 | # Building server, final dest is /dist 23 | RUN npm install && npm run build 24 | RUN rm -rf node_modules && npm install --omit=dev 25 | 26 | FROM base AS runner 27 | RUN apk --no-cache add curl 28 | LABEL org.opencontainers.image.source=https://github.com/dhenry123/utdon 29 | LABEL org.opencontainers.image.description="Multi arch image" 30 | LABEL org.opencontainers.image.licenses=AGPLV3 31 | 32 | ARG RUNASUSER 33 | ARG RUNASUSERID 34 | ARG RUNASGROUP 35 | 36 | # Creating user & group 37 | RUN addgroup -S ${RUNASUSER} --gid "${RUNASGROUP}" && adduser -S ${RUNASUSER} -s /bin/sh --uid "${RUNASUSERID}" -G ${RUNASUSER} 38 | 39 | USER ${RUNASUSERID} 40 | 41 | WORKDIR /app 42 | 43 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/dist/ ./ 44 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/openapi.yaml ./ 45 | COPY --from=builder --chown=${RUNASUSERID}:${RUNASGROUP} /app/node_modules/ ./node_modules 46 | # UI must be built by developer 47 | COPY --chown=${RUNASUSERID}:${RUNASGROUP} ./client/dist/ ./public 48 | 49 | # data directory for mount point 50 | RUN mkdir data 51 | 52 | EXPOSE 3015 53 | 54 | CMD ["node","main.js"] 55 | -------------------------------------------------------------------------------- /build-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @author DHENRY for mytinydc.com 3 | # @license AGPL3 4 | 5 | set -e 6 | 7 | # create this file and set env LOCALREGISTRY=[Your local container registry] 8 | source .envlocaldev 9 | 10 | # jq is needed 11 | which jq >/dev/null 2>&1 12 | if [ "$?" == "1" ]; then 13 | echo "You have to install jq package" 14 | exit 1 15 | fi 16 | TAG=$(jq '.version' package.json | sed -E 's/^"|"$//g') 17 | PROGRESS="--progress plain" 18 | NOCACHE="--no-cache" 19 | PLATFORM="--platform=linux/arm64" 20 | echo "Building image $LOCALREGISTRY:$TAG" 21 | sudo docker buildx build --load $PROGRESS $NOCACHE $PLATFORM -t $LOCALREGISTRY:$TAG -f Dockerfile-dev . 22 | echo "Pushing image $LOCALREGISTRY:$TAG" 23 | sudo docker push "$LOCALREGISTRY":"$TAG" 24 | -------------------------------------------------------------------------------- /build-prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @author DHENRY for mytinydc.com 3 | # @license AGPL3 4 | 5 | set -e 6 | 7 | source .envlocaldev 8 | # login to github 9 | echo $CR_PAT | sudo docker login ghcr.io -u $USERNAME --password-stdin 10 | 11 | # Prepare buildx multiarch 12 | sudo docker buildx rm multiarch 13 | sudo docker buildx create --name multiarch --use 14 | 15 | # jq is needed 16 | which jq >/dev/null 2>&1 17 | if [ "$?" == "1" ]; then 18 | echo "You have to install jq package" 19 | exit 1 20 | fi 21 | NOCACHE="--no-cache" 22 | PLATFORM="--platform=linux/arm64,linux/amd64" 23 | TAG=$(jq '.version' package.json | sed -E 's/^"|"$//g') 24 | PROGRESS="--progress plain" 25 | sudo docker buildx build --push $PROGRESS $NOCACHE $PLATFORM -t ghcr.io/$USERNAME/utdon:$TAG -f Dockerfile . 26 | -------------------------------------------------------------------------------- /cacerts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/cacerts/.keep -------------------------------------------------------------------------------- /checkHardCoded.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | env="./.envTest" 5 | if [ ! -f "${env}" ]; then 6 | echo "You have to set env file (from ${env}-sample)" 7 | fi 8 | 9 | . "${env}" 10 | 11 | files=$(git diff --name-only | xargs) 12 | for word in ${HARCODEDGREP}; do 13 | for file in ${files}; do 14 | if grep "${word}" "${file}" >/dev/null; then 15 | echo "Word: ${word} found in file: ${file}" 16 | fi 17 | done 18 | done 19 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'], 5 | ignorePatterns: ['dist', '.eslintrc.cjs','TPL.tsx'], 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['react-refresh'], 8 | rules: { 9 | 'react-refresh/only-export-components': [ 10 | 'warn', 11 | { allowConstantExport: true }, 12 | ], 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | storybook-static 26 | -------------------------------------------------------------------------------- /client/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | core: { 6 | disableTelemetry: true, // 👈 Disables telemetry 7 | }, 8 | addons: [ 9 | "@storybook/addon-links", 10 | "@storybook/addon-essentials", 11 | "@storybook/addon-interactions", 12 | "storybook-addon-remix-react-router", 13 | ], 14 | framework: { 15 | name: "@storybook/react-vite", 16 | options: {}, 17 | }, 18 | docs: { 19 | autodocs: "tag", 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /client/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { IntlProvider } from "react-intl"; 3 | import { Provider } from "react-redux"; 4 | import { store } from "../src/app/store"; 5 | import French from "../../locales/fr.json"; 6 | 7 | // icons 8 | import "@tabler/icons-webfont/tabler-icons.min.css"; 9 | import React from "react"; 10 | 11 | import "../src/app/css/root.scss"; 12 | 13 | const preview: Preview = { 14 | parameters: { 15 | controls: { 16 | matchers: { 17 | color: /(background|color)$/i, 18 | date: /Date$/i, 19 | }, 20 | }, 21 | }, 22 | decorators: [ 23 | (Story) => ( 24 | 25 | {}}> 26 | 27 | 28 | 29 | ), 30 | ], 31 | }; 32 | 33 | export default preview; 34 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | UPTODATE OR NOT [UTDON] 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mytinydc-utdon-client", 3 | "private": true, 4 | "version": "1.9.0", 5 | "description": "Application for tracking obsolete FOSS applications - UI", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview", 12 | "test": "jest", 13 | "storybook": "storybook dev --no-open -p 6106", 14 | "build-storybook": "storybook build", 15 | "AddComponent": "tools/addComponents.sh" 16 | }, 17 | "dependencies": { 18 | "@reduxjs/toolkit": "^1.9.7", 19 | "@tabler/icons-webfont": "^2.47.0", 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1", 22 | "react-intl": "^6.8.9", 23 | "react-multi-select-component": "^4.3.4", 24 | "react-redux": "^8.1.3", 25 | "react-router-dom": "^6.29.0" 26 | }, 27 | "devDependencies": { 28 | "@storybook/addon-essentials": "^8.5.3", 29 | "@storybook/addon-interactions": "^8.5.3", 30 | "@storybook/addon-links": "^8.5.3", 31 | "@storybook/blocks": "^8.0.8", 32 | "@storybook/preview-api": "^8.3.1", 33 | "@storybook/react": "^8.0.8", 34 | "@storybook/react-vite": "^8.5.3", 35 | "@storybook/test": "^8.0.8", 36 | "@types/node": "^22.13.1", 37 | "@types/react": "^18.3.18", 38 | "@types/react-dom": "^18.3.5", 39 | "@typescript-eslint/eslint-plugin": "^6.21.0", 40 | "@typescript-eslint/parser": "^6.21.0", 41 | "@vitejs/plugin-react": "^4.3.4", 42 | "eslint": "^8.57.1", 43 | "eslint-plugin-react-hooks": "^4.6.2", 44 | "eslint-plugin-react-refresh": "^0.4.18", 45 | "eslint-plugin-storybook": "^0.8.0", 46 | "sass": "^1.83.4", 47 | "storybook": "^8.5.3", 48 | "storybook-addon-remix-react-router": "^3.1.0", 49 | "typescript": "^5.7.3", 50 | "vite": "^4.5.9" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/dhenry123/utdon.git" 55 | }, 56 | "author": "DHENRY for mytinydc.com", 57 | "license": "AGPL-3.0" 58 | } 59 | -------------------------------------------------------------------------------- /client/public/assets/MytinyDC_logo-brand-full-darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/client/public/assets/MytinyDC_logo-brand-full-darkmode.png -------------------------------------------------------------------------------- /client/public/assets/MytinyDC_logo-brand-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/client/public/assets/MytinyDC_logo-brand-full.png -------------------------------------------------------------------------------- /client/public/assets/download_image_1700775996997.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/client/public/assets/download_image_1700775996997.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | // icons 7 | import "@tabler/icons-webfont/tabler-icons.min.css"; 8 | 9 | import { useAppDispatch, useAppSelector } from "./app/hook"; 10 | import { IntlProvider } from "react-intl"; 11 | import ServiceMessage from "./components/ServiceMessage"; 12 | import { RouterProvider } from "react-router-dom"; 13 | import { Router } from "./app/Router"; 14 | import { useEffect } from "react"; 15 | import { setIsLoaderShip, setLanguage } from "./app/contextSlice"; 16 | import { Dialog } from "./components/Dialog"; 17 | 18 | import "./app/css/loadership.scss"; 19 | 20 | export const App = () => { 21 | const dispatch = useAppDispatch(); 22 | const contextLanguage = useAppSelector((state) => state.context.language); 23 | 24 | const isDialogVisible = useAppSelector((state) => state.context.isLoaderShip); 25 | 26 | useEffect(() => { 27 | // Browser language detection 28 | const navigatorLocale = 29 | navigator.language.split("-")[0].toLowerCase() !== "fr" ? "en" : "fr"; 30 | dispatch(setLanguage(navigatorLocale)); 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, []); 33 | 34 | return ( 35 |
36 | {}} 40 | > 41 | {/* Route change in regard the value of "contextIsLogged" */} 42 | 43 | {/* Global Service Messenger */} 44 | 45 | dispatch(setIsLoaderShip(false))} 49 | sticky={true} 50 | > 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /client/src/app/Router.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { createBrowserRouter } from "react-router-dom"; 7 | import { ErrorInRouter } from "../features/errors/ErrorInRouter"; 8 | import { PageLogin } from "../features/login/PageLogin"; 9 | import { useAppDispatch } from "../app/hook"; 10 | import { mytinydcUPDONApi } from "../api/mytinydcUPDONApi"; 11 | import { showServiceMessage } from "./serviceMessageSlice"; 12 | import { PageHome } from "../features/homepage/PageHome"; 13 | import { DisplayControls } from "../features/displaycontrols/DisplayControls"; 14 | import { ControlManager } from "../features/controlmanagement/ControlManager"; 15 | import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; 16 | 17 | /** 18 | * Logic : 19 | * user ask route 20 | * @returns 21 | */ 22 | export const Router = () => { 23 | const dispatch = useAppDispatch(); 24 | 25 | return createBrowserRouter([ 26 | { 27 | path: "/", 28 | element: , 29 | errorElement: , 30 | loader: async () => { 31 | return await dispatch( 32 | mytinydcUPDONApi.endpoints.getUserIsAuthenticated.initiate(null) 33 | ) 34 | .unwrap() 35 | .catch((error: FetchBaseQueryError) => { 36 | if (error.status === 401) { 37 | return ; 38 | } else { 39 | dispatch( 40 | showServiceMessage({ 41 | detail: 42 | error && error.data 43 | ? error.data.toString() 44 | : "Unknown check server logs", 45 | }) 46 | ); 47 | } 48 | }); 49 | }, 50 | children: [ 51 | { 52 | path: "/ui/addcontrol", 53 | element: , 54 | errorElement: , 55 | }, 56 | { 57 | path: "/ui/editcontrol/:uuid", 58 | element: , 59 | errorElement: , 60 | }, 61 | 62 | { 63 | path: "/", 64 | element: , 65 | errorElement: , 66 | }, 67 | ], 68 | }, 69 | 70 | { 71 | path: "/login", 72 | element: , 73 | errorElement: , 74 | }, 75 | ]); 76 | }; 77 | -------------------------------------------------------------------------------- /client/src/app/contextSlice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { createSlice } from "@reduxjs/toolkit"; 7 | import languageFr from "../../../locales/fr.json"; 8 | import { INITIALIZED_UPTODATEFORM } from "../../../src/Constants"; 9 | import { 10 | contextSliceType, 11 | DisplayControlsType, 12 | } from "../../../src/Global.types"; 13 | 14 | const initialState: contextSliceType = { 15 | // French is default language 16 | language: { locale: "fr", lang: languageFr }, 17 | application: { 18 | name: "UTdOn", 19 | applicationtitle: "UtDon", 20 | copyright: "December 2023", 21 | licence: "AGPL-3.0", 22 | }, 23 | uptodateForm: INITIALIZED_UPTODATEFORM, 24 | refetchuptodateForm: false, 25 | isAdmin: false, 26 | search: "", 27 | isLoaderShip: false, 28 | displayControlsType: localStorage.getItem( 29 | "displayControlsAsList" 30 | ) as DisplayControlsType, 31 | authToken: "", 32 | }; 33 | 34 | export const contextSlice = createSlice({ 35 | name: "context", 36 | initialState, 37 | reducers: { 38 | setLanguage: (state, value) => { 39 | if (value.payload === "fr") { 40 | state.language.locale = value.payload; 41 | state.language.lang = languageFr; 42 | } else { 43 | //default is en 44 | state.language.locale = "en"; 45 | state.language.lang = {}; 46 | } 47 | }, 48 | updateKeyUptodateFrom(state, value) { 49 | if (value.payload.key) 50 | state.uptodateForm = { 51 | ...state.uptodateForm, 52 | [value.payload.key]: value.payload.value, 53 | }; 54 | }, 55 | setUpdateForm(state, value) { 56 | state.uptodateForm = value.payload; 57 | }, 58 | resetUpdateForm(state) { 59 | state.uptodateForm = INITIALIZED_UPTODATEFORM; 60 | }, 61 | setRefetchuptodateForm(state, value) { 62 | state.refetchuptodateForm = value.payload; 63 | }, 64 | setIsAdmin(state, value) { 65 | state.isAdmin = value.payload || false; 66 | }, 67 | setSearch(state, value) { 68 | state.search = value.payload; 69 | }, 70 | setIsLoaderShip(state, value) { 71 | state.isLoaderShip = value.payload; 72 | }, 73 | setDisplayControlsAsList(state, value) { 74 | localStorage.setItem("displayControlsAsList", value.payload); 75 | state.displayControlsType = value.payload; 76 | }, 77 | setAuthToken(state, value) { 78 | state.authToken = value.payload; 79 | }, 80 | }, 81 | }); 82 | 83 | // Exportable actions 84 | export const { 85 | setLanguage, 86 | updateKeyUptodateFrom, 87 | resetUpdateForm, 88 | setUpdateForm, 89 | setRefetchuptodateForm, 90 | setIsAdmin, 91 | setSearch, 92 | setIsLoaderShip, 93 | setDisplayControlsAsList, 94 | setAuthToken, 95 | } = contextSlice.actions; 96 | export default contextSlice.reducer; 97 | -------------------------------------------------------------------------------- /client/src/app/css/loadership.scss: -------------------------------------------------------------------------------- 1 | @use "root.scss"; 2 | .loadership_Dialog { 3 | .modal-content { 4 | background-color: transparent !important; 5 | } 6 | 7 | .loadership_ILQMG { 8 | display: flex; 9 | position: relative; 10 | width: 54px; 11 | height: 54px; 12 | } 13 | 14 | .loadership_ILQMG div { 15 | position: absolute; 16 | width: 18px; 17 | height: 18px; 18 | background: root.$loadership-background-color; 19 | animation: loadership_ILQMG_scale 1.9s infinite, 20 | loadership_ILQMG_fade 1.9s infinite; 21 | animation-timing-function: ease-in-out; 22 | } 23 | 24 | .loadership_ILQMG div:nth-child(1) { 25 | animation-delay: 0s; 26 | top: 0px; 27 | left: 0px; 28 | } 29 | .loadership_ILQMG div:nth-child(2) { 30 | animation-delay: 0.15s; 31 | top: 0px; 32 | left: 18px; 33 | } 34 | .loadership_ILQMG div:nth-child(3) { 35 | animation-delay: 0.3s; 36 | top: 0px; 37 | left: 36px; 38 | } 39 | .loadership_ILQMG div:nth-child(4) { 40 | animation-delay: 0.15s; 41 | top: 18px; 42 | left: 0px; 43 | } 44 | .loadership_ILQMG div:nth-child(5) { 45 | animation-delay: 0.3s; 46 | top: 18px; 47 | left: 18px; 48 | } 49 | .loadership_ILQMG div:nth-child(6) { 50 | animation-delay: 0.45s; 51 | top: 18px; 52 | left: 36px; 53 | } 54 | .loadership_ILQMG div:nth-child(7) { 55 | animation-delay: 0.3s; 56 | top: 36px; 57 | left: 0px; 58 | } 59 | .loadership_ILQMG div:nth-child(8) { 60 | animation-delay: 0.45s; 61 | top: 36px; 62 | left: 18px; 63 | } 64 | .loadership_ILQMG div:nth-child(9) { 65 | animation-delay: 0.6s; 66 | top: 36px; 67 | left: 36px; 68 | } 69 | 70 | @keyframes loadership_ILQMG_scale { 71 | 0%, 72 | 47.36842105263158%, 73 | 100% { 74 | transform: scale(1); 75 | } 76 | 23.68421052631579% { 77 | transform: scale(0); 78 | } 79 | } 80 | 81 | @keyframes loadership_ILQMG_fade { 82 | 0%, 83 | 47.36842105263158%, 84 | 100% { 85 | opacity: 1; 86 | } 87 | 23.68421052631579% { 88 | opacity: 1; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/src/app/hook.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import type { TypedUseSelectorHook } from "react-redux"; 8 | import type { RootState, AppDispatch } from "./store"; 9 | 10 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 11 | export const useAppDispatch: () => AppDispatch = useDispatch; 12 | export const useAppSelector: TypedUseSelectorHook = useSelector; 13 | -------------------------------------------------------------------------------- /client/src/app/serviceMessageSlice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { createSlice } from "@reduxjs/toolkit"; 7 | import { INITIALIZED_TOAST } from "../../../src/Constants"; 8 | 9 | const initialState = { 10 | toast: INITIALIZED_TOAST, 11 | }; 12 | 13 | const formatDetail = (message: string) => { 14 | return message + " (" + new Date().toLocaleTimeString() + ")"; 15 | }; 16 | 17 | export const serviceMessageSlice = createSlice({ 18 | name: "servicemessage", 19 | initialState, 20 | reducers: { 21 | /** 22 | * possibility to send message with full object or simply with only one string 23 | * @param {object} state 24 | * @param {object || string} value 25 | * @returns 26 | */ 27 | showServiceMessage: (state, value) => { 28 | if (typeof value.payload === "object" && value.payload) { 29 | const newToast = { ...value.payload }; 30 | newToast.detail = formatDetail(newToast.detail); 31 | // timestamp must be set here to apply changes on toast timestamp listen by serviceMessage 32 | newToast.timestamp = new Date().valueOf(); 33 | state.toast = newToast; 34 | } else { 35 | console.error("toast is not Object"); 36 | } 37 | }, 38 | /** 39 | * send signal to clear visually all toasts 40 | * @param state 41 | */ 42 | clearToast: (state) => { 43 | const newToast = { ...INITIALIZED_TOAST, empty: true }; 44 | newToast.timestamp = new Date().valueOf(); 45 | state.toast = newToast; 46 | }, 47 | }, 48 | }); 49 | 50 | // Exportable actions 51 | export const { showServiceMessage, clearToast } = serviceMessageSlice.actions; 52 | export default serviceMessageSlice.reducer; 53 | -------------------------------------------------------------------------------- /client/src/app/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { configureStore } from "@reduxjs/toolkit"; 7 | import context from "./contextSlice"; 8 | import { mytinydcUPDONApi } from "../api/mytinydcUPDONApi"; 9 | import servicemessage from "./serviceMessageSlice"; 10 | 11 | export const store = configureStore({ 12 | reducer: { 13 | context: context, 14 | servicemessage: servicemessage, 15 | // Services 16 | [mytinydcUPDONApi.reducerPath]: mytinydcUPDONApi.reducer, 17 | }, 18 | // Adding the api middleware enables caching, invalidation, polling, 19 | // and other useful features of `rtk-query`. 20 | middleware: (getDefaultMiddleware) => 21 | getDefaultMiddleware().concat(mytinydcUPDONApi.middleware), 22 | }); 23 | 24 | // Infer the `RootState` and `AppDispatch` types from the store itself 25 | export type RootState = ReturnType; 26 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 27 | export type AppDispatch = typeof store.dispatch; 28 | -------------------------------------------------------------------------------- /client/src/components/ActionsSettings.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .ActionsSettings { 3 | .Block { 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | h2 { 7 | width: 100%; 8 | } 9 | .url, 10 | .auth { 11 | flex-grow: 1; 12 | .inputgeneric { 13 | width: 90%; 14 | } 15 | } 16 | .nextstep { 17 | display: flex; 18 | flex-direction: row; 19 | width: max-content; 20 | align-self: center; 21 | flex-grow: 1; 22 | justify-content: right; 23 | .ButtonGeneric { 24 | width: max-content; 25 | margin: 0 8px; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/ActionsSettings.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ActionsSettings } from "./ActionsSettings"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { STORYBOOK_UPTODATEFORM } from "../../../src/Constants-dev"; 9 | import { fn } from "@storybook/test"; 10 | 11 | const meta = { 12 | title: "Forms/ActionsSettings", 13 | component: ActionsSettings, 14 | decorators: [withRouter], 15 | parameters: { 16 | layout: "fullscreen", 17 | reactRouter: reactRouterParameters({ 18 | location: { path: "/" }, 19 | }), 20 | }, 21 | tags: ["autodocs"], 22 | argTypes: {}, 23 | } satisfies Meta; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | export const Primary: Story = { 29 | args: { 30 | activeUptodateForm: { ...STORYBOOK_UPTODATEFORM }, 31 | handleOnChange: fn(), 32 | onDone: fn(), 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/components/Badge.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .Badge { 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | column-gap: 0; 8 | height: 2rem; 9 | min-width: 9rem; 10 | .label { 11 | background-color: root.$badge-background-color; 12 | color: root.$badge-label-color; 13 | padding: root.$padding-badge; 14 | text-transform: lowercase; 15 | height: inherit; 16 | width: max-content; 17 | border-radius: root.$border-radius-badge 0 0 root.$border-radius-badge; 18 | display: flex; 19 | align-items: center; 20 | @media (prefers-color-scheme: dark) { 21 | color: root.$badge-label-color-dark; 22 | } 23 | } 24 | .value { 25 | padding: root.$padding-badge; 26 | border-radius: 0 root.$border-radius-badge root.$border-radius-badge 0; 27 | font-weight: bold; 28 | height: inherit; 29 | width: 100%; 30 | display: flex; 31 | align-items: center; 32 | &.uptodate { 33 | background-color: root.$badge-background-color-success; 34 | color: root.$badge-color-success; 35 | @media (prefers-color-scheme: dark) { 36 | color: root.$badge-color-success-dark; 37 | } 38 | } 39 | &.toupdate { 40 | background-color: root.$badge-background-color-error; 41 | color: root.$badge-color-error; 42 | @media (prefers-color-scheme: dark) { 43 | color: root.$badge-color-error-dark; 44 | } 45 | } 46 | &.uptodatewithwarn { 47 | background-color: root.$badge-background-color-warning; 48 | color: root.$badge-color-warning; 49 | @media (prefers-color-scheme: dark) { 50 | color: root.$badge-color-warning-dark; 51 | } 52 | } 53 | &.nostate { 54 | background-color: root.$badge-background-color-nostate; 55 | color: root.$badge-color-nostate; 56 | @media (prefers-color-scheme: dark) { 57 | color: root.$badge-color-nostate-dark; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/src/components/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Badge } from "./Badge"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/Badge", 11 | component: Badge, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const UpToDate: Story = { 27 | args: { 28 | isSuccess: true, 29 | }, 30 | }; 31 | 32 | export const ToUpDate: Story = { 33 | args: { 34 | isSuccess: false, 35 | }, 36 | }; 37 | 38 | export const Warn: Story = { 39 | args: { 40 | isWarning: true, 41 | isSuccess: true, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /client/src/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./Badge.scss"; 7 | 8 | interface BadgeProps { 9 | isSuccess: boolean; 10 | isWarning?: boolean; 11 | onClick?: (event: React.MouseEvent) => void; 12 | title?: string; 13 | noState?: boolean; 14 | } 15 | export const Badge = ({ 16 | isSuccess, 17 | isWarning, 18 | onClick, 19 | title, 20 | noState, 21 | }: BadgeProps) => { 22 | return ( 23 |
{}} 26 | title={title ? title : ""} 27 | > 28 |
State
29 |
40 | {isSuccess ? "UP to date" : noState ? "No State" : "OUT of date"} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /client/src/components/Block.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .Block { 3 | min-width: 256px; 4 | display: flex; 5 | flex-direction: row; 6 | box-shadow: root.$boxshadow-default; 7 | padding: root.$padding-default; 8 | margin: root.$margin-default; 9 | border: root.$border-default; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | .Block { 14 | box-shadow: root.$boxshadow-default-dark; 15 | border: root.$border-default-dark; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/Block.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Block } from "./Block"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/Block", 11 | component: Block, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | children:
This div is included in this block
, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/Block.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./Block.scss"; 7 | 8 | interface BlockProps { 9 | children: JSX.Element | string | JSX.Element[]; 10 | className?: string; 11 | } 12 | export const Block = ({ children, className }: BlockProps) => { 13 | return ( 14 |
{children}
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/components/ButtonGeneric.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .ButtonGeneric { 4 | background-color: root.$button-generic-background-color; 5 | color: root.$button-generic-color; 6 | border-radius: root.$border-radius-button; 7 | border: none; 8 | padding: 0.5rem; 9 | font-weight: bold; 10 | height: max-content; 11 | display: flex; 12 | column-gap: 0.5rem; 13 | align-items: center; 14 | width: max-content; 15 | 16 | &:not(:disabled) > .label:hover { 17 | cursor: pointer; 18 | } 19 | &.disabled, 20 | &:disabled > .label { 21 | background-color: root.$background-color-warning !important; 22 | color: root.$color-text-secondary !important; 23 | &:hover { 24 | cursor: not-allowed; 25 | } 26 | } 27 | &.round { 28 | border-radius: root.$border-radius-round; 29 | } 30 | &.success { 31 | background-color: root.$color-success; 32 | color: root.$color-text-secondary; 33 | } 34 | &.warning { 35 | background-color: root.$background-color-warning; 36 | color: root.$color-text-secondary; 37 | } 38 | } 39 | .ButtonGeneric:hover { 40 | cursor: pointer; 41 | } 42 | .ButtonGeneric:focus { 43 | border-color: root.$button-generic-border-color-focus; 44 | } 45 | -------------------------------------------------------------------------------- /client/src/components/ButtonGeneric.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import ButtonGeneric from "./ButtonGeneric"; 4 | 5 | const meta = { 6 | title: "Components/Input/ButtonGeneric", 7 | component: ButtonGeneric, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Primary: Story = { 19 | args: { 20 | label: "label", 21 | onClick: () => {}, 22 | }, 23 | }; 24 | 25 | export const Success: Story = { 26 | args: { 27 | label: "label", 28 | className: "success", 29 | onClick: () => {}, 30 | }, 31 | }; 32 | 33 | export const Warning: Story = { 34 | args: { 35 | label: "label", 36 | className: "warning", 37 | onClick: () => {}, 38 | }, 39 | }; 40 | 41 | export const WithIcon: Story = { 42 | args: { 43 | label: "label", 44 | onClick: () => {}, 45 | icon: "ti ti-360", 46 | }, 47 | }; 48 | 49 | export const OnlyIcon: Story = { 50 | args: { 51 | onClick: () => {}, 52 | icon: "ti ti-360", 53 | }, 54 | }; 55 | 56 | export const RoundOnlyIcon: Story = { 57 | args: { 58 | onClick: () => {}, 59 | icon: "ti ti-360", 60 | className: "round", 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /client/src/components/ButtonGeneric.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./ButtonGeneric.scss"; 7 | 8 | interface ButtonGenericProps { 9 | onClick: React.MouseEventHandler; 10 | title?: string; 11 | /** 12 | * enter only the name of Tabler icon 13 | */ 14 | icon?: string; 15 | label?: string; 16 | className?: string; 17 | disabled?: boolean; 18 | autoFocus?: boolean; 19 | onMouseDown?: () => void; 20 | onMouseUP?: () => void; 21 | } 22 | 23 | const ButtonGeneric = ({ 24 | onClick, 25 | title, 26 | icon, 27 | label, 28 | className, 29 | disabled = false, 30 | autoFocus = false, 31 | onMouseDown, 32 | onMouseUP, 33 | }: ButtonGenericProps) => { 34 | return ( 35 | 51 | ); 52 | }; 53 | 54 | export default ButtonGeneric; 55 | -------------------------------------------------------------------------------- /client/src/components/CheckBox.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .CheckBox { 3 | display: flex; 4 | color: inherit; 5 | @media (prefers-color-scheme: dark) { 6 | color: root.$color-text-primary-dark; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/src/components/CheckBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { CheckBox } from "./CheckBox"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Components/Input/CheckBox", 11 | component: CheckBox, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | // If you need to keep state in storybook, you also could use the app redux store 27 | export const Primary: Story = { 28 | args: { 29 | checked: true, 30 | label: "checkbox, move cursor hover the check to display title", 31 | title: "checkbox title", 32 | onChange: () => {}, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/components/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./CheckBox.scss"; 7 | import { ChangeEvent } from "react"; 8 | 9 | interface CheckBoxProps { 10 | label?: string; 11 | title?: string; 12 | checked: boolean; 13 | onChange: (event: ChangeEvent) => void; 14 | } 15 | export const CheckBox = ({ 16 | label, 17 | title, 18 | checked, 19 | onChange, 20 | }: CheckBoxProps) => { 21 | return ( 22 |
23 | 24 | {label ?
{label}
: null} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/ConfirmDialog.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .ConfirmDialog { 3 | display: flex; 4 | flex-direction: column; 5 | .message { 6 | padding: root.$padding-default; 7 | margin: root.$margin-default; 8 | } 9 | .groupButtons { 10 | display: flex; 11 | flex-direction: row !important; 12 | column-gap: root.$columngap-default; 13 | justify-content: center; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/ConfirmDialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ConfirmDialog } from "./ConfirmDialog"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/ConfirmDialog", 11 | component: ConfirmDialog, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | visible: true, 29 | message: "test", 30 | onConfirm: () => {}, 31 | onCancel: () => {}, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /client/src/components/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | 8 | import "./ConfirmDialog.scss"; 9 | import { Dialog } from "./Dialog"; 10 | import ButtonGeneric from "./ButtonGeneric"; 11 | 12 | interface ConfirmDialogProps { 13 | visible: boolean; 14 | message: string; 15 | onConfirm: () => void; 16 | onCancel: () => void; 17 | } 18 | 19 | export const ConfirmDialog = ({ 20 | visible, 21 | message, 22 | onCancel, 23 | onConfirm, 24 | }: ConfirmDialogProps) => { 25 | const intl = useIntl(); 26 | 27 | return ( 28 | {}} 31 | header={intl.formatMessage({ id: "Confirmation" })} 32 | > 33 |
34 |
{message}
35 |
36 | 40 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /client/src/components/Control.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .Control { 3 | display: flex; 4 | flex-direction: column; 5 | border-radius: root.$border-radius-control; 6 | padding: 1rem; 7 | width: 20vw; 8 | .identity { 9 | display: flex; 10 | flex-direction: row; 11 | .appLogo { 12 | align-self: center; 13 | filter: grayscale(100%); 14 | min-width: 90px; 15 | min-height: 90px; 16 | .content { 17 | min-width: 90px; 18 | min-height: 90px; 19 | img { 20 | width: 90px; 21 | height: 90px; 22 | @media (prefers-color-scheme: dark) { 23 | filter: invert(1); 24 | } 25 | } 26 | } 27 | } 28 | .nameuuid { 29 | flex-grow: 1; 30 | display: flex; 31 | flex-direction: column; 32 | overflow: hidden; 33 | .name { 34 | flex-grow: 1; 35 | font-weight: bold; 36 | overflow: hidden; 37 | white-space: nowrap; 38 | text-overflow: ellipsis; 39 | } 40 | .uuid { 41 | font-size: 0.7rem; 42 | } 43 | } 44 | } 45 | .lastestCompare { 46 | .details { 47 | display: flex; 48 | align-items: center; 49 | column-gap: root.$columngap-default; 50 | .Badge:hover { 51 | cursor: pointer; 52 | } 53 | } 54 | } 55 | .ControlGroupButtons { 56 | margin-top: 1rem; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/components/Control.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Control } from "./Control"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { STORYBOOK_UPTODATEFORM } from "../../../src/Constants-dev"; 9 | 10 | const meta = { 11 | title: "Ui/Control", 12 | component: Control, 13 | decorators: [withRouter], 14 | parameters: { 15 | layout: "fullscreen", 16 | reactRouter: reactRouterParameters({ 17 | location: { path: "/" }, 18 | }), 19 | }, 20 | tags: ["autodocs"], 21 | argTypes: {}, 22 | } satisfies Meta; 23 | 24 | export default meta; 25 | type Story = StoryObj; 26 | 27 | export const Primary: Story = { 28 | args: { 29 | data: { ...STORYBOOK_UPTODATEFORM }, 30 | 31 | handleOnDelete: () => {}, 32 | handleOnCompare: () => {}, 33 | handleOnPause: () => {}, 34 | handleOnEdit: () => {}, 35 | handleOnCurlCommands: () => {}, 36 | setConfirmDeleteIsVisible: () => {}, 37 | confirmDeleteIsVisible: false, 38 | setIsDialogCompareVisible: () => {}, 39 | setResultCompare: () => {}, 40 | handleOnDuplicate: () => {}, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/components/ControlGroupButtons.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .ControlGroupButtons { 3 | display: flex; 4 | flex-direction: column; 5 | row-gap: root.$rowgap-default; 6 | .groupButtons { 7 | display: flex; 8 | flex-direction: row; 9 | column-gap: root.$columngap-default; 10 | justify-content: center; 11 | flex-grow: 1; 12 | .buttons { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | column-gap: 1rem; 17 | row-gap: 1rem; 18 | flex-wrap: wrap; 19 | padding: 0.7rem; 20 | width: 95%; 21 | margin-top: auto; 22 | .ButtonGeneric { 23 | border-radius: root.$border-radius-round; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/ControlGroupButtons.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { ControlGroupButtons } from "./ControlGroupButtons"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | import { STORYBOOK_UPTODATEFORM } from "../../../src/Constants-dev"; 10 | 11 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 12 | const meta = { 13 | title: "UI/ControlGroupButtons", 14 | component: ControlGroupButtons, 15 | decorators: [withRouter], 16 | parameters: { 17 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 18 | layout: "fullscreen", 19 | reactRouter: reactRouterParameters({ 20 | location: { path: "/" }, 21 | }), 22 | }, 23 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 24 | tags: ["autodocs"], 25 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 26 | argTypes: {}, 27 | } satisfies Meta; 28 | 29 | export default meta; 30 | type Story = StoryObj; 31 | 32 | // If you need to keep state in storybook, you also could use the app redux store 33 | // const Component = ({ ...args }) => { 34 | // const [, setArgs] = useArgs(); 35 | // const onChange = (value: string) => { 36 | // // Call the provided callback 37 | // // This is used for the Actions tab 38 | // args.onChange?.(value); 39 | // 40 | // // Update the arg in Storybook 41 | // setArgs({ value }); 42 | // }; 43 | // return ; 44 | // }; 45 | export const Primary: Story = { 46 | args: { 47 | data: { ...STORYBOOK_UPTODATEFORM }, 48 | handleOnEdit: () => {}, 49 | setConfirmDeleteIsVisible: () => {}, 50 | handleOnCurlCommands: () => {}, 51 | handleOnCompare: () => {}, 52 | handleOnPause: () => {}, 53 | handleOnDuplicate: () => {}, 54 | }, 55 | // if you need to get a specific render see SelectArs component... 56 | // render: (args) => Component(args), 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/components/ControlGroupButtons.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | import "./ControlGroupButtons.scss"; 8 | import ButtonGeneric from "./ButtonGeneric"; 9 | import { CheckBox } from "./CheckBox"; 10 | import { UptodateForm } from "../../../src/Global.types"; 11 | import { ChangeEvent } from "react"; 12 | 13 | interface ControlGroupButtonsProps { 14 | data: UptodateForm; 15 | handleOnDelete: (control: UptodateForm) => void; 16 | handleOnEdit: (control: UptodateForm) => void; 17 | handleOnCurlCommands: (control: UptodateForm) => void; 18 | handleOnCompare: (control: UptodateForm) => void; 19 | handleOnPause: ( 20 | event: ChangeEvent, 21 | control: UptodateForm 22 | ) => void; 23 | handleOnDuplicate: (control: UptodateForm) => void; 24 | } 25 | 26 | export const ControlGroupButtons = ({ 27 | data, 28 | handleOnEdit, 29 | handleOnDelete, 30 | handleOnCurlCommands, 31 | handleOnCompare, 32 | handleOnPause, 33 | handleOnDuplicate, 34 | }: ControlGroupButtonsProps) => { 35 | const intl = useIntl(); 36 | 37 | return ( 38 |
39 |
40 | handleOnEdit(data)} 43 | icon="pencil" 44 | /> 45 | handleOnDuplicate(data)} 48 | icon="copy" 49 | /> 50 | handleOnDelete(data)} 54 | icon="trash" 55 | /> 56 | handleOnCurlCommands(data)} 59 | icon="slashes" 60 | /> 61 | handleOnCompare(data)} 65 | icon="git-compare" 66 | /> 67 |
68 | { 71 | handleOnPause(event, data); 72 | }} 73 | title={intl.formatMessage({ 74 | id: "In the case of a global selection, only the comparison will be processed", 75 | })} 76 | checked={data.isPause} 77 | /> 78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /client/src/components/Dialog.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .Dialog { 3 | // inspired from https://www.w3schools.com/howto/howto_css_modals.asp 4 | .modal-common { 5 | position: fixed; 6 | z-index: 10; 7 | left: 0; 8 | top: 0; 9 | width: 100%; 10 | height: 100%; 11 | &.modal { 12 | opacity: root.$dialog-opacity; 13 | background-color: root.$dialog-color-background; 14 | } 15 | } 16 | .modal-container { 17 | background-color: transparent; 18 | display: flex; 19 | align-items: center; 20 | overflow: hidden; 21 | z-index: 11; 22 | .modal-content { 23 | position: relative; 24 | background-color: root.$background-color; 25 | border-radius: root.$border-radius-default; 26 | margin: auto; 27 | padding: root.$padding-default; 28 | width: max-content; 29 | width: auto; 30 | max-width: 80vw; 31 | -webkit-animation-name: animatetop; 32 | -webkit-animation-duration: 0.4s; 33 | animation-name: animatetop; 34 | animation-duration: 0.4s; 35 | max-height: 90vh; 36 | overflow-y: auto; 37 | border: root.$border-default; 38 | @media (prefers-color-scheme: dark) { 39 | background-color: root.$background-color-dark; 40 | color: root.$color-text-primary-dark; 41 | border: root.$border-default-dark; 42 | } 43 | 44 | .modal-header { 45 | padding: 0 0.5rem; 46 | border-radius: root.$border-radius-default root.$border-radius-default 0 0; 47 | color: root.$dialog-header-color; 48 | display: flex; 49 | align-items: start; 50 | min-width: 30vw; 51 | @media (prefers-color-scheme: dark) { 52 | background-color: root.$background-color-dark; 53 | color: root.$color-text-primary-dark; 54 | } 55 | h2 { 56 | text-transform: none; 57 | font-size: 1rem; 58 | border-bottom: none; 59 | flex: 1; 60 | margin: 0px 0; 61 | } 62 | .modal-close { 63 | flex: 0; 64 | width: 0.6rem; 65 | height: 0.6rem; 66 | margin-top: 0; 67 | justify-content: center; 68 | background-color: root.$dialog-header-button-background-color; 69 | color: root.$dialog-header-button-color; 70 | border: none; 71 | border-radius: root.$border-radius-round; 72 | .ti { 73 | font-size: 0.7rem; 74 | } 75 | } 76 | .modal-close:hover { 77 | cursor: pointer; 78 | } 79 | } 80 | .modal-body { 81 | border-radius: 0 0 root.$border-radius-default root.$border-radius-default; 82 | } 83 | .modal-footer { 84 | display: flex; 85 | justify-content: center; 86 | margin: root.$margin-default; 87 | padding: root.$padding-default; 88 | } 89 | } 90 | } 91 | 92 | @-webkit-keyframes animatetop { 93 | from { 94 | top: -300px; 95 | opacity: 0; 96 | } 97 | to { 98 | top: 0; 99 | opacity: 1; 100 | } 101 | } 102 | 103 | @keyframes animatetop { 104 | from { 105 | top: -300px; 106 | opacity: 0; 107 | } 108 | to { 109 | top: 0; 110 | opacity: 1; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /client/src/components/Dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Dialog } from "./Dialog"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { fn } from "@storybook/test"; 9 | 10 | const meta = { 11 | title: "Ui/Dialog", 12 | component: Dialog, 13 | decorators: [withRouter], 14 | parameters: { 15 | layout: "fullscreen", 16 | reactRouter: reactRouterParameters({ 17 | location: { path: "/" }, 18 | }), 19 | }, 20 | tags: ["autodocs"], 21 | argTypes: {}, 22 | } satisfies Meta; 23 | 24 | export default meta; 25 | type Story = StoryObj; 26 | 27 | export const Primary: Story = { 28 | args: { 29 | children:
TEST
, 30 | visible: true, 31 | header: "Header Dialog", 32 | closeButton: true, 33 | onHide: fn() 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./Dialog.scss"; 7 | import ButtonGeneric from "./ButtonGeneric"; 8 | import { useIntl } from "react-intl"; 9 | 10 | interface DialogInterface { 11 | children?: JSX.Element | string | JSX.Element[]; 12 | onHide: () => void; 13 | visible: boolean; 14 | closeButton?: boolean; 15 | header?: string; 16 | footerClose?: boolean; 17 | className?: string; 18 | sticky?: boolean; 19 | } 20 | 21 | export const Dialog = ({ 22 | children, 23 | closeButton = false, 24 | onHide, 25 | visible, 26 | header = "", 27 | footerClose = false, 28 | className, 29 | sticky = false, 30 | }: DialogInterface) => { 31 | const intl = useIntl(); 32 | 33 | if (visible) { 34 | return ( 35 |
36 |
37 |
{ 39 | if (!sticky) onHide(); 40 | }} 41 | className={`modal-common modal-container ${ 42 | className ? className : "" 43 | }`} 44 | > 45 |
) => { 48 | event.preventDefault(); 49 | event.stopPropagation(); 50 | }} 51 | > 52 | <> 53 | {header ? ( 54 |
55 |

{header}

56 | {closeButton ? ( 57 | 62 | ) : null} 63 |
64 | ) : null} 65 | 66 |
{children}
67 | {footerClose ? ( 68 |
69 | 73 |
74 | ) : null} 75 |
76 |
77 |
78 | ); 79 | } else { 80 | return null; 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /client/src/components/DisplayVersions.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .DisplayVersions { 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | .productionVersion, 7 | .githubLatestRelease { 8 | white-space: nowrap; 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | max-width: 4rem; 12 | } 13 | .separator { 14 | padding: 0 0.2rem 0 0.2rem; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/components/DisplayVersions.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { DisplayVersions } from "./DisplayVersions"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "NewComponent/DisplayVersions", 13 | component: DisplayVersions, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: { 46 | data: { 47 | compareResult: { 48 | productionVersion: "V prod", 49 | githubLatestRelease: "V git", 50 | }, 51 | }, 52 | }, 53 | // if you need to get a specific render see SelectArs component... 54 | // render: (args) => Component(args), 55 | }; 56 | -------------------------------------------------------------------------------- /client/src/components/DisplayVersions.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | 8 | import "./DisplayVersions.scss"; 9 | 10 | interface DisplayVersionsProps { 11 | data: any; 12 | } 13 | export const DisplayVersions = ({ data }: DisplayVersionsProps) => { 14 | const intl = useIntl(); 15 | 16 | return ( 17 |
18 | {data.compareResult && data.compareResult.productionVersion ? ( 19 | <> 20 |
26 | {data.compareResult.productionVersion} 27 |
28 |
/
29 |
35 | {data.compareResult.githubLatestRelease} 36 |
37 | 38 | ) : ( 39 |
"No version detected"
40 | )} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /client/src/components/FieldSet.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .FieldSet { 3 | // important to use ellipsis 4 | min-width: 0; 5 | legend { 6 | display: flex; 7 | flex-direction: row; 8 | font-weight: bold; 9 | .IconWithTooltip { 10 | margin: 0 0.5rem; 11 | } 12 | } 13 | .content { 14 | max-width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | 19 | @media (prefers-color-scheme: dark) { 20 | color: root.$color-text-primary-dark; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/FieldSet.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { FieldSet } from "./FieldSet"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/FieldSet", 11 | component: FieldSet, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | legend: "title fieldset", 29 | children:
any component
, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /client/src/components/FieldSet.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./FieldSet.scss"; 7 | import { IconWithTooltip } from "./IconWithTooltip"; 8 | 9 | export interface FieldSetProps { 10 | children?: JSX.Element | string | JSX.Element[]; 11 | legend: string; 12 | className?: string; 13 | toolTipIcon?: string; 14 | toolTipContent?: string; 15 | } 16 | export const FieldSet = ({ 17 | legend, 18 | children, 19 | className, 20 | toolTipIcon = "help", 21 | toolTipContent, 22 | }: FieldSetProps) => { 23 | return ( 24 |
25 | 26 | {legend} 27 | {toolTipContent ? ( 28 | 29 | ) : null} 30 | 31 |
{children}
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/components/FieldSetApiEntrypoint.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .FieldSetApiEntrypoint { 3 | .curltitle { 4 | font-weight: bold; 5 | margin-top: root.$margin-default; 6 | } 7 | .curlcommand { 8 | padding: root.$padding-default; 9 | position: relative; 10 | min-height: 3rem; 11 | border: root.$border-default; 12 | .command { 13 | margin-top: 1rem; 14 | padding: 0; 15 | } 16 | .copyToClipboard { 17 | position: absolute; 18 | top: 2px; 19 | right: 2px; 20 | border-radius: root.$border-radius-round; 21 | background-color: root.$color-error; 22 | color: root.$color-text-secondary; 23 | padding: 0.3rem; 24 | .ti { 25 | font-size: 0.8rem; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/FieldSetApiEntrypoint.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { FieldSetApiEntrypoint } from "./FieldSetApiEntrypoint"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/FieldSetApiEntrypoint", 11 | component: FieldSetApiEntrypoint, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const OneControl: Story = { 27 | args: { 28 | apiEntrypoint: "/api/v1/action/compare/xxxx/0", 29 | method: "GET", 30 | commandTitle: "test", 31 | userAuthToken: "xxxx", 32 | }, 33 | }; 34 | 35 | export const AllControl: Story = { 36 | args: { 37 | apiEntrypoint: "/api/v1/action/compare/all/0", 38 | method: "GET", 39 | commandTitle: "test", 40 | userAuthToken: "xxxx", 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/components/FieldSetAuthorizationHeader.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .FieldSetAuthorizationHeader { 3 | .authToken { 4 | padding: root.$padding-default; 5 | position: relative; 6 | border: root.$border-default; 7 | 8 | .copyToClipboard { 9 | position: absolute; 10 | top: 2px; 11 | right: 2px; 12 | border-radius: root.$border-radius-round; 13 | background-color: root.$color-error; 14 | color: root.$color-text-secondary; 15 | padding: 0.3rem; 16 | .ti { 17 | font-size: 0.8rem; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/components/FieldSetAuthorizationHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { FieldSetAuthorizationHeader } from "./FieldSetAuthorizationHeader"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/FieldSetAuthorizationHeader", 11 | component: FieldSetAuthorizationHeader, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | authToken: "xxxxxxxxx", 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/FieldSetAuthorizationHeader.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | import { useAppDispatch } from "../app/hook"; 8 | 9 | import "./FieldSetAuthorizationHeader.scss"; 10 | import { FieldSet } from "./FieldSet"; 11 | import { copyToClipboard } from "../helpers/UiMiscHelper"; 12 | import { showServiceMessage } from "../app/serviceMessageSlice"; 13 | import { INITIALIZED_TOAST } from "../../../src/Constants"; 14 | import ButtonGeneric from "./ButtonGeneric"; 15 | import { useRef } from "react"; 16 | 17 | interface FieldSetAuthorizationHeaderProps { 18 | authToken: string; 19 | } 20 | export const FieldSetAuthorizationHeader = ({ 21 | authToken, 22 | }: FieldSetAuthorizationHeaderProps) => { 23 | const intl = useIntl(); 24 | const dispatch = useAppDispatch(); 25 | const divRef = useRef(null); 26 | 27 | const handleOnCopyToClipboard = async () => { 28 | copyToClipboard(divRef) 29 | .then(() => { 30 | dispatch( 31 | showServiceMessage({ 32 | ...INITIALIZED_TOAST, 33 | severity: "info", 34 | detail: intl.formatMessage({ 35 | id: "The command has been copied to Clipboard", 36 | }), 37 | }) 38 | ); 39 | }) 40 | .catch((error: Error) => { 41 | dispatch( 42 | showServiceMessage({ 43 | ...INITIALIZED_TOAST, 44 | severity: "error", 45 | detail: `Unexpected Error: ${error.toString()}`, 46 | }) 47 | ); 48 | }); 49 | }; 50 | 51 | return ( 52 |
56 |
57 |
{`Authorization: ${authToken}`}
58 | 66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /client/src/components/FieldSetClickableUrl.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .FieldSetClickableUrl { 3 | } 4 | -------------------------------------------------------------------------------- /client/src/components/FieldSetClickableUrl.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { FieldSetClickableUrl } from "./FieldSetClickableUrl"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Ui/FieldSetClickableUrl", 11 | component: FieldSetClickableUrl, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | url: "http://test.com", 29 | legend: "myurl", 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /client/src/components/FieldSetClickableUrl.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./FieldSetClickableUrl.scss"; 7 | import { FieldSet, FieldSetProps } from "./FieldSet"; 8 | import { UrlOpener } from "./UrlOpener"; 9 | 10 | interface FieldSetClickableUrlProps extends FieldSetProps { 11 | url: string; 12 | } 13 | export const FieldSetClickableUrl = ({ 14 | ...props 15 | }: FieldSetClickableUrlProps) => { 16 | return ( 17 |
23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/components/GlobalGithubToken.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .GlobalGithubToken { 3 | align-self: center; 4 | justify-self: center; 5 | width: 40vw; 6 | &.Block { 7 | display: flex; 8 | flex-direction: column; 9 | .groupButtons { 10 | margin: root.$margin-default; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | justify-content: center; 15 | column-gap: root.$columngap-default; 16 | } 17 | .githubtoken { 18 | .content { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: left; 23 | column-gap: root.$columngap-default; 24 | .explain { 25 | width: 100%; 26 | padding: 0.5rem; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | .input { 31 | display: flex; 32 | flex-direction: row; 33 | width: 75%; 34 | justify-items: center; 35 | align-items: center; 36 | column-gap: root.$columngap-default; 37 | .inputgeneric { 38 | width: auto; 39 | flex-grow: 1; 40 | &.framed { 41 | border-color: red; 42 | } 43 | } 44 | .ButtonGeneric { 45 | border-radius: root.$border-radius-round; 46 | width: 2rem; 47 | height: 2rem; 48 | i { 49 | font-size: 1rem; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/src/components/GlobalGithubToken.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { GlobalGithubToken } from "./GlobalGithubToken"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "Forms/GlobalGithubToken", 13 | component: GlobalGithubToken, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | export const Primary: Story = { 32 | args: { 33 | handleOnPost: () => {}, 34 | onHide: () => {}, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/components/Header.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .header { 4 | padding-top: 0.3rem; 5 | width: 100%; 6 | background-color: root.$background-color; 7 | z-index: 10; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | @media (prefers-color-scheme: dark) { 15 | background-color: root.$background-color-dark; 16 | } 17 | .flexPushLeft { 18 | margin-left: auto; 19 | 20 | .loginName { 21 | display: flex; 22 | margin-top: auto; 23 | margin-bottom: auto; 24 | margin-right: 0.5rem; 25 | 26 | font-size: 1.2rem; 27 | 28 | @media (prefers-color-scheme: dark) { 29 | color: root.$color-text-primary-dark; 30 | } 31 | } 32 | } 33 | .ti { 34 | font-size: 1.7rem; 35 | } 36 | .logo { 37 | cursor: pointer; 38 | width: 180px; 39 | height: 50px; 40 | align-self: flex-start; 41 | background-image: root.$logo-background-image; 42 | background-repeat: no-repeat; 43 | margin: 4px; 44 | cursor: pointer; 45 | background-color: inherit; 46 | padding: 6px 6px 0 0; 47 | opacity: 0.7; 48 | 49 | @media (prefers-color-scheme: dark) { 50 | background-image: root.$logo-background-image-dark; 51 | background-size: 180px 50px; 52 | } 53 | } 54 | .buttonsgroup { 55 | display: flex; 56 | flex-direction: row; 57 | column-gap: 4px; 58 | align-items: center; 59 | width: 100%; 60 | button { 61 | border-radius: root.$border-radius-round; 62 | } 63 | .buttonlogout { 64 | border-radius: root.$border-radius-button; 65 | max-width: 7rem; 66 | .label { 67 | width: 100%; 68 | overflow: hidden; 69 | white-space: nowrap; 70 | text-overflow: ellipsis; 71 | } 72 | } 73 | .apicontrols, 74 | .manager { 75 | display: inherit; 76 | column-gap: inherit; 77 | } 78 | .apicontrols, 79 | .addcontrol { 80 | margin-left: 2rem; 81 | } 82 | .manager { 83 | margin-right: 2rem; 84 | } 85 | .logout { 86 | display: flex; 87 | margin-right: 1rem; 88 | column-gap: inherit; 89 | } 90 | } 91 | .borderLeft { 92 | background-color: inherit; 93 | width: 2px; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /client/src/components/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Header } from "./Header"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Components/Navigation/Header", 11 | component: Header, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const MobileNormalUser: Story = { 27 | args: { 28 | isAdmin: false, 29 | isMobile: true, 30 | }, 31 | }; 32 | 33 | export const MobileAdinistrator: Story = { 34 | args: { 35 | isAdmin: true, 36 | isMobile: true, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /client/src/components/HttpHeader.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .HttpHeader { 3 | .content { 4 | display: flex; 5 | flex-direction: row; 6 | column-gap: 1rem; 7 | .inputgeneric::placeholder { 8 | color: root.$color-placeholder; 9 | font-weight: bold; 10 | font-size: 0.7rem; 11 | } 12 | .headerhttpkey { 13 | width: 15rem !important; 14 | } 15 | .headerhttpvalue { 16 | flex-grow: 1; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/HttpHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { HttpHeader } from "./HttpHeader"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "UI/HttpHeader", 13 | component: HttpHeader, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: { 46 | handleOnChange: () => {}, 47 | headerkey: "", 48 | headervalue: "", 49 | headerkeyField: "exprGithub", 50 | headervalueField: "exprGithub", 51 | }, 52 | // if you need to get a specific render see SelectArs component... 53 | // render: (args) => Component(args), 54 | }; 55 | -------------------------------------------------------------------------------- /client/src/components/HttpHeader.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | 8 | import "./HttpHeader.scss"; 9 | import { FieldSet } from "./FieldSet"; 10 | import InputGeneric from "./InputGeneric"; 11 | import { UptodateFormFields } from "../../../src/Global.types"; 12 | interface HttpHeaderProps { 13 | handleOnChange: (key: UptodateFormFields, value: string | string[]) => void; 14 | headerkey: string; 15 | headervalue: string; 16 | headerkeyField: UptodateFormFields; 17 | headervalueField: UptodateFormFields; 18 | disabled?: boolean; 19 | } 20 | export const HttpHeader = ({ 21 | handleOnChange, 22 | headerkeyField, 23 | headervalueField, 24 | headerkey, 25 | headervalue, 26 | disabled, 27 | }: HttpHeaderProps) => { 28 | const intl = useIntl(); 29 | 30 | return ( 31 |
Attribute: Authorization ; value: Bearer [your github token]", 46 | })} (${intl.formatMessage({ id: "Bearer is important" })}) 47 | `} 48 | > 49 | handleOnChange(headerkeyField, value)} 55 | disabled={disabled} 56 | /> 57 | handleOnChange(headervalueField, value)} 67 | disabled={disabled} 68 | /> 69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /client/src/components/IconWithTooltip.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .IconWithTooltip { 3 | .tooltip { 4 | position: absolute; 5 | z-index: 9999; /* Set a high z-index value */ 6 | background-color: root.$background-color; 7 | border: root.$border-default; 8 | padding: 5px; 9 | border-radius: root.$border-radius-default; 10 | box-shadow: root.$boxshadow-default; 11 | display: flex; 12 | flex-direction: column; 13 | @media (prefers-color-scheme: dark) { 14 | background-color: root.$background-color-dark; 15 | border: root.$border-default-dark; 16 | } 17 | .tooltiptitle { 18 | border-bottom: root.$border-default; 19 | margin: 0.5rem; 20 | align-self: center; 21 | @media (prefers-color-scheme: dark) { 22 | border-bottom: root.$border-default-dark; 23 | } 24 | } 25 | .tooltipbody { 26 | margin: 1rem; 27 | font-weight: normal; 28 | } 29 | } 30 | i { 31 | font-size: 1.2rem; 32 | color: root.$color-info; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/src/components/IconWithTooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { IconWithTooltip } from "./IconWithTooltip"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "UI/IconWithTooltip", 13 | component: IconWithTooltip, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: { 46 | icon: "help", 47 | tooltipContent: "test", 48 | }, 49 | // if you need to get a specific render see SelectArs component... 50 | // render: (args) => Component(args), 51 | }; 52 | -------------------------------------------------------------------------------- /client/src/components/IconWithTooltip.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | 8 | import "./IconWithTooltip.scss"; 9 | import { useState } from "react"; 10 | interface IconWithTooltipProps { 11 | icon: string; 12 | tooltipContent: string; 13 | } 14 | 15 | export const IconWithTooltip = ({ 16 | icon, 17 | tooltipContent, 18 | }: IconWithTooltipProps) => { 19 | const intl = useIntl(); 20 | 21 | const [showTooltip, setShowTooltip] = useState(false); 22 | const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); 23 | 24 | const handleMouseEnter = (event: React.MouseEvent) => { 25 | const { clientX, clientY } = event; 26 | setShowTooltip(true); 27 | setTooltipPosition({ x: clientX, y: clientY }); 28 | }; 29 | 30 | const handleMouseLeave = () => { 31 | setShowTooltip(false); 32 | }; 33 | 34 | return ( 35 |
40 | {icon ? : ""} 41 | {showTooltip && ( 42 |
46 |
47 | {intl.formatMessage({ id: "Help" })} 48 |
49 |
50 | {tooltipContent && 51 | tooltipContent.split("\n").map((line, idx) => { 52 | return
{line}
; 53 | })} 54 |
55 |
56 | )} 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /client/src/components/ImageUploader.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .ImageUploader { 4 | .inputUploadNative { 5 | display: none; 6 | } 7 | .dragDropButtonContainer { 8 | position: relative; 9 | width: root.$app-logo-width; 10 | height: root.$app-logo-height; 11 | } 12 | .imagePpreview { 13 | width: root.$app-logo-width; 14 | height: root.$app-logo-height; 15 | background-color: transparent; 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | z-index: 0; 20 | img { 21 | width: inherit; 22 | height: inherit; 23 | } 24 | } 25 | .dragDropButton { 26 | width: root.$app-logo-width; 27 | height: root.$app-logo-height; 28 | background-color: transparent; 29 | position: absolute; 30 | border: root.$border-dashed; 31 | top: 0; 32 | left: 0; 33 | z-index: 1; 34 | @media (prefers-color-scheme: dark) { 35 | border: root.$border-dashed-dark; 36 | } 37 | .upload-button { 38 | cursor: pointer; 39 | 40 | padding: 0.25rem; 41 | font-size: 0.7rem; 42 | border: none; 43 | font-family: "Oswald", sans-serif; 44 | background-color: transparent; 45 | width: root.$app-logo-width; 46 | height: root.$app-logo-height; 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | } 50 | .upload-button:hover { 51 | background-color: root.$background-color; 52 | font-weight: bold; 53 | opacity: 0.6; 54 | @media (prefers-color-scheme: dark) { 55 | background-color: root.$background-color-dark; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/components/ImageUploader.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ImageUploader } from "./ImageUploader"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { useState } from "react"; 9 | import React from "react"; 10 | 11 | const meta = { 12 | title: "Components/Input/ImageUploader", 13 | component: ImageUploader, 14 | decorators: [withRouter], 15 | parameters: { 16 | layout: "fullscreen", 17 | reactRouter: reactRouterParameters({ 18 | location: { path: "/" }, 19 | }), 20 | }, 21 | tags: ["autodocs"], 22 | } satisfies Meta; 23 | 24 | export default meta; 25 | type Story = StoryObj; 26 | 27 | const Component = ({ ...args }) => { 28 | const [image, setImage] = useState(args.image); 29 | 30 | const onChange = (value: string) => { 31 | setImage(value); 32 | }; 33 | 34 | return ( 35 | 41 | ); 42 | }; 43 | 44 | export const Primary: Story = { 45 | args: { 46 | image: "", 47 | onError: (error: string) => { 48 | console.log(error); 49 | }, 50 | onChange: (image: string) => { 51 | console.log(image); 52 | }, 53 | }, 54 | render: (args) => Component(args), 55 | }; 56 | -------------------------------------------------------------------------------- /client/src/components/InputGeneric.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .inputgeneric { 3 | padding: 0.5rem 0.5rem; 4 | transition: background-color 0.2s, color 0.2s, border-color 0.2s, 5 | box-shadow 0.2s; 6 | appearance: none; 7 | border-radius: root.$border-radius-default; 8 | border: root.$border-default; 9 | @media (prefers-color-scheme: dark) { 10 | border: root.$border-default-dark; 11 | } 12 | &:focus { 13 | outline: root.$input-outline-default; 14 | } 15 | } 16 | .inputgeneric::placeholder { 17 | color: root.$color-placeholder; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/InputGeneric.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import InputGeneric from "./InputGeneric"; 4 | import { useArgs } from "@storybook/preview-api"; 5 | import React from "react"; 6 | 7 | const meta = { 8 | title: "Components/Input/InputGeneric", 9 | component: InputGeneric, 10 | parameters: { 11 | layout: "centered", 12 | }, 13 | tags: ["autodocs"], 14 | argTypes: {}, 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | function Component({ ...args }) { 21 | const [, setArgs] = useArgs(); 22 | 23 | const onChange = (value: string) => { 24 | args.onChange?.(value); 25 | setArgs({ value }); 26 | }; 27 | 28 | return ; 29 | } 30 | 31 | export const Primary: Story = { 32 | args: { 33 | placeholder: "Placeholder type your text", 34 | value: "", 35 | onChange: (event) => { 36 | console.log(event); 37 | }, 38 | }, 39 | render: Component, 40 | }; 41 | -------------------------------------------------------------------------------- /client/src/components/InputGeneric.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useEffect } from "react"; 7 | import "./InputGeneric.scss"; 8 | 9 | export interface InputGenericProps { 10 | value: string; 11 | onChange: (value: string) => void; 12 | type?: React.HTMLInputTypeAttribute; 13 | title?: string; 14 | ref?: React.LegacyRef; 15 | className?: string; 16 | autoComplete?: string; 17 | placeholder?: string; 18 | autoFocus?: boolean; 19 | onBlur?: React.FocusEventHandler; 20 | onKeyUp?: (keyboardKeyAsString: string) => void; 21 | onKeyDown?: (keyboardKeyAsString: string) => void; 22 | disabled?: boolean; 23 | } 24 | 25 | const InputGeneric = ({ 26 | type = "text", 27 | title, 28 | value, 29 | className, 30 | autoComplete = "on", 31 | placeholder, 32 | autoFocus = false, 33 | onChange, 34 | onBlur, 35 | onKeyUp, 36 | onKeyDown, 37 | ref, 38 | disabled, 39 | }: InputGenericProps) => { 40 | useEffect(() => { 41 | onChange(value); 42 | // eslint-disable-next-line react-hooks/exhaustive-deps 43 | }, [value]); 44 | 45 | return ( 46 | target.value 51 | onChange={(e) => onChange(e.target.value)} 52 | onBlur={onBlur} 53 | onKeyUp={(event) => (onKeyUp ? onKeyUp(event.key) : null)} 54 | onKeyDown={(event) => (onKeyDown ? onKeyDown(event.key) : null)} 55 | autoComplete={autoComplete} 56 | ref={ref} 57 | placeholder={placeholder} 58 | title={title} 59 | // Only one per page !!!! 60 | autoFocus={autoFocus} 61 | disabled={disabled} 62 | /> 63 | ); 64 | }; 65 | 66 | export default InputGeneric; 67 | -------------------------------------------------------------------------------- /client/src/components/InputIcon.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .InputIcon { 3 | display: flex; 4 | flex-direction: row; 5 | column-gap: 0.5rem; 6 | align-items: center; 7 | margin: root.$margin-default; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/components/InputIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { InputIcon } from "./InputIcon"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Components/Input/InputIcon", 11 | component: InputIcon, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | placeholder: "Placeholder type your text", 29 | value: "", 30 | onChange: (event) => { 31 | console.log(event); 32 | }, 33 | icon: "plus", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/components/InputIcon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./InputIcon.scss"; 7 | import InputGeneric, { InputGenericProps } from "./InputGeneric"; 8 | 9 | interface InputIconProps extends InputGenericProps { 10 | icon: string; 11 | } 12 | 13 | export const InputIcon = ({ ...props }: InputIconProps) => { 14 | return ( 15 |
16 | 17 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/LoginBlock.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .LoginBlock { 4 | display: flex; 5 | flex-direction: column; 6 | row-gap: root.$rowgap-default; 7 | column-gap: root.$columngap-default; 8 | max-width: 35vw; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/LoginBlock.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { LoginBlock } from "./LoginBlock"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Forms/LoginBlock", 11 | component: LoginBlock, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | onLogin: (data) => { 29 | console.log(data); 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/components/LoginBlock.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useState } from "react"; 7 | import { useIntl } from "react-intl"; 8 | import { useAppSelector } from "../app/hook"; 9 | import ButtonGeneric from "./ButtonGeneric"; 10 | import { Block } from "./Block"; 11 | import { InputIcon } from "./InputIcon"; 12 | import { PostAuthent } from "../../../src/Global.types"; 13 | 14 | import "./LoginBlock.scss"; 15 | 16 | export interface LoginBlockProps { 17 | onLogin: (data: PostAuthent) => void; 18 | } 19 | export const LoginBlock = ({ onLogin }: LoginBlockProps) => { 20 | const intl = useIntl(); 21 | 22 | const applicationContext = useAppSelector( 23 | (state) => state.context.application 24 | ); 25 | const [login, setLogin] = useState(""); 26 | const [password, setPassword] = useState(""); 27 | 28 | const handleOnLogin = () => { 29 | if (login && password) onLogin({ login: login, password: password }); 30 | }; 31 | return ( 32 | 33 |

{applicationContext.name}

34 | 35 | 42 | 43 | { 51 | if (key == "Enter") handleOnLogin(); 52 | }} 53 | /> 54 | 55 |
56 | 60 |
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /client/src/components/ResultCompare.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .ResultCompare { 3 | display: flex; 4 | row-gap: root.$rowgap-default; 5 | .warning { 6 | font-weight: bold; 7 | color: root.$color-error; 8 | } 9 | .operation { 10 | height: max-content; 11 | .groupButtons { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | row-gap: 0.5rem; 16 | button { 17 | width: 100%; 18 | } 19 | } 20 | } 21 | .result { 22 | .Badge { 23 | .value { 24 | width: max-content; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/ResultCompare.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ResultCompare } from "./ResultCompare"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { 9 | STORYBOOK_UPDATEORNOTSTATE, 10 | STORYBOOK_UPTODATEFORM, 11 | } from "../../../src/Constants-dev"; 12 | 13 | const meta = { 14 | title: "Ui/ResultCompare", 15 | component: ResultCompare, 16 | decorators: [withRouter], 17 | parameters: { 18 | layout: "fullscreen", 19 | reactRouter: reactRouterParameters({ 20 | location: { path: "/" }, 21 | }), 22 | }, 23 | tags: ["autodocs"], 24 | argTypes: {}, 25 | } satisfies Meta; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const Primary: Story = { 31 | args: { 32 | control: STORYBOOK_UPTODATEFORM, 33 | result: STORYBOOK_UPDATEORNOTSTATE, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/components/ScrapGitHubReleaseTags.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .ScrapGitHubReleaseTags { 4 | display: flex; 5 | flex-direction: column; 6 | row-gap: root.$rowgap-default; 7 | padding: root.$padding-default; 8 | width: 100%; 9 | .Block { 10 | align-items: center; 11 | .FieldSet { 12 | align-self: flex-start; 13 | } 14 | &.inactive { 15 | display: none; 16 | } 17 | &.filter { 18 | flex-direction: row; 19 | align-items: flex-start; 20 | .list { 21 | height: 20vh; 22 | overflow-y: auto; 23 | overflow-x: hidden; 24 | &.excluded { 25 | color: root.$color-error; 26 | font-weight: bold; 27 | } 28 | } 29 | .version { 30 | max-width: 19rem; 31 | font-weight: bold; 32 | .error { 33 | color: root.$color-error; 34 | } 35 | .success { 36 | color: root.$color-success; 37 | } 38 | } 39 | } 40 | .expression { 41 | flex-grow: 1; 42 | } 43 | .urlGithub { 44 | flex-grow: 1; 45 | .inputgeneric { 46 | width: 60%; 47 | } 48 | .HttpHeader { 49 | width: 80%; 50 | } 51 | } 52 | .content { 53 | width: 100%; 54 | .scrapContent { 55 | border: root.$border-default; 56 | border-radius: root.$border-radius-default; 57 | width: 99%; 58 | height: root.$form-scrap-content-height; 59 | padding: root.$padding-default; 60 | overflow-x: hidden; 61 | overflow-y: auto; 62 | @media (prefers-color-scheme: dark) { 63 | color: root.$color-text-primary-dark; 64 | border: root.$border-default-dark; 65 | } 66 | } 67 | } 68 | .nextstep { 69 | display: flex; 70 | flex-direction: row; 71 | width: max-content; 72 | align-self: center; 73 | flex-grow: 1; 74 | justify-content: right; 75 | .ButtonGeneric { 76 | width: max-content; 77 | margin: 0 8px; 78 | } 79 | } 80 | .getcontent { 81 | margin: 0 8px; 82 | width: 10rem; 83 | word-wrap: break-word; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /client/src/components/ScrapProduction.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .ScrapProduction { 4 | display: flex; 5 | flex-direction: column; 6 | row-gap: root.$rowgap-default; 7 | padding: root.$padding-default; 8 | width: 100%; 9 | .nameandfixed { 10 | display: flex; 11 | flex-direction: column; 12 | width: auto; 13 | margin-right: 2rem; 14 | .FieldSet { 15 | width: 100% !important; 16 | } 17 | } 18 | .Block { 19 | flex-wrap: wrap; 20 | align-items: center; 21 | .FieldSet { 22 | align-self: flex-start; 23 | } 24 | &.filter { 25 | flex-direction: row; 26 | } 27 | .flogo { 28 | align-self: center; 29 | } 30 | .expression { 31 | flex-grow: 1; 32 | } 33 | .ImageUploader { 34 | display: flex; 35 | justify-content: center; 36 | width: 25%; 37 | } 38 | 39 | .name { 40 | width: 25%; 41 | .inputgeneric { 42 | width: 90%; 43 | } 44 | } 45 | .url { 46 | flex-grow: 1; 47 | .inputgeneric { 48 | width: 90%; 49 | } 50 | } 51 | .getcontent { 52 | margin: 0 8px; 53 | width: 10rem; 54 | word-wrap: break-word; 55 | } 56 | .nextstep { 57 | display: flex; 58 | flex-direction: row; 59 | width: max-content; 60 | align-self: center; 61 | flex-grow: 1; 62 | justify-content: right; 63 | .ButtonGeneric { 64 | width: max-content; 65 | margin: 0 8px; 66 | } 67 | } 68 | .groups { 69 | min-width: 15rem; 70 | .dropdown-container { 71 | border: root.$border-default; 72 | color: root.$color-text-primary; 73 | @media (prefers-color-scheme: dark) { 74 | border: root.$border-default-dark; 75 | background-color: root.$background-color-dark; 76 | color: root.$color-text-primary-dark; 77 | } 78 | .dropdown-content * { 79 | color: root.$color-text-primary; 80 | @media (prefers-color-scheme: dark) { 81 | background-color: root.$background-color-dark; 82 | color: root.$color-text-primary-dark; 83 | border: none; 84 | } 85 | } 86 | } 87 | } 88 | .content { 89 | width: 100%; 90 | .scrapContent { 91 | border: root.$border-default; 92 | border-radius: root.$border-radius-default; 93 | width: 100%; 94 | height: root.$form-scrap-content-height; 95 | padding: root.$padding-default; 96 | overflow-x: hidden; 97 | overflow-y: auto; 98 | 99 | @media (prefers-color-scheme: dark) { 100 | color: root.$color-text-primary-dark; 101 | border: root.$border-default-dark; 102 | } 103 | } 104 | } 105 | .version { 106 | max-width: 19rem; 107 | font-weight: bold; 108 | .error { 109 | color: root.$color-error; 110 | } 111 | .success { 112 | color: root.$color-success; 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /client/src/components/ScrapProduction.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ScrapProduction, ScrapProductionProps } from "./ScrapProduction"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { UptodateForm, UptodateFormFields } from "../../../src/Global.types"; 9 | import { INITIALIZED_UPTODATEFORM } from "../../../src/Constants"; 10 | import { useState } from "react"; 11 | import { fn } from "@storybook/test"; 12 | 13 | const meta = { 14 | title: "Forms/ScrapProduction", 15 | component: ScrapProduction, 16 | decorators: [withRouter], 17 | parameters: { 18 | layout: "fullscreen", 19 | reactRouter: reactRouterParameters({ 20 | location: { path: "/" }, 21 | }), 22 | }, 23 | args: { 24 | onDone: fn(), 25 | handleOnChange: fn(), 26 | displayError: fn(), 27 | }, 28 | tags: ["autodocs"], 29 | argTypes: {}, 30 | } satisfies Meta; 31 | 32 | export default meta; 33 | type Story = StoryObj; 34 | 35 | const activeUptodateForm: UptodateForm = { 36 | ...INITIALIZED_UPTODATEFORM, 37 | urlProduction: "https://test.com", 38 | scrapTypeProduction: "text", 39 | exprProduction: "version: (v[\\d.]+)", 40 | }; 41 | 42 | const Component = (args: ScrapProductionProps) => { 43 | const [control, setControl] = useState(args.activeUptodateForm); 44 | args = { 45 | ...args, 46 | handleOnChange: (key: UptodateFormFields, value: string | string[]) => { 47 | // defined by stories 48 | if (key === "scrapTypeProduction") return; 49 | setControl({ ...control, [key]: value }); 50 | }, 51 | }; 52 | return ; 53 | }; 54 | 55 | export const ScrapAsText: Story = { 56 | args: { 57 | activeUptodateForm: { ...activeUptodateForm, scrapTypeProduction: "text" }, 58 | scrapUrl: async (url: string) => { 59 | return new Promise((resolv) => { 60 | resolv(` 61 | 62 | url asked : ${url} 63 | 64 | test version: v3.0.1 65 | 66 | 67 | `); 68 | }); 69 | }, 70 | }, 71 | render: (args) => Component(args), 72 | }; 73 | 74 | export const ScrapAsJSON: Story = { 75 | args: { 76 | activeUptodateForm: { ...activeUptodateForm, scrapTypeProduction: "json" }, 77 | scrapUrl: () => { 78 | return new Promise((resolv) => { 79 | resolv( 80 | JSON.stringify({ 81 | version: "3.0.1", 82 | }), 83 | ); 84 | }); 85 | }, 86 | }, 87 | render: (args) => Component(args), 88 | }; 89 | -------------------------------------------------------------------------------- /client/src/components/Search.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | $searchbuttonsize: 2rem; 3 | .Search { 4 | margin-left: 2rem; 5 | display: flex; 6 | flex-direction: row; 7 | column-gap: 0.2rem; 8 | width: 20%; 9 | border: root.$border-search; 10 | border-radius: root.$border-radius-default; 11 | padding: 0 0.2rem; 12 | @media (prefers-color-scheme: dark) { 13 | border: root.$border-search-dark; 14 | } 15 | .inputgeneric { 16 | border: none; 17 | width: 100%; 18 | } 19 | .inputgeneric:focus { 20 | outline: none; 21 | } 22 | .ButtonGeneric { 23 | width: $searchbuttonsize; 24 | height: $searchbuttonsize; 25 | align-self: center; 26 | background-color: transparent; 27 | i { 28 | font-size: 1rem; 29 | color: root.$button-search-color; 30 | @media (prefers-color-scheme: dark) { 31 | color: root.$button-search-color-dark; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/Search.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { Search } from "./Search"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "Components/Input/Search", 13 | component: Search, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: { 46 | searchString: "", 47 | }, 48 | // if you need to get a specific render see SelectArs component... 49 | // render: (args) => Component(args), 50 | }; 51 | -------------------------------------------------------------------------------- /client/src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | import { useAppDispatch } from "../app/hook"; 8 | 9 | import "./Search.scss"; 10 | import InputGeneric from "./InputGeneric"; 11 | import { setSearch } from "../app/contextSlice"; 12 | import ButtonGeneric from "./ButtonGeneric"; 13 | 14 | interface SearchProps { 15 | searchString: string; 16 | } 17 | export const Search = ({ searchString }: SearchProps) => { 18 | const intl = useIntl(); 19 | const dispatch = useAppDispatch(); 20 | 21 | return ( 22 |
23 | { 27 | dispatch(setSearch(value)); 28 | }} 29 | autoComplete="off" 30 | className="inputsearch" 31 | autoFocus 32 | /> 33 | {searchString ? ( 34 | { 37 | dispatch(setSearch("")); 38 | }} 39 | title={intl.formatMessage({ id: "Reset" })} 40 | /> 41 | ) : null} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /client/src/components/SelectGeneric.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .SelectGeneric { 3 | padding: 0.5rem 0.5rem; 4 | transition: background-color 0.2s, color 0.2s, border-color 0.2s, 5 | box-shadow 0.2s; 6 | appearance: none; 7 | border-radius: root.$border-radius-default; 8 | border: root.$border-default; 9 | @media (prefers-color-scheme: dark) { 10 | border: root.$border-default-dark; 11 | } 12 | &:focus { 13 | outline: root.$input-outline-default; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/SelectGeneric.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import SelectGeneric from "./SelectGeneric"; 4 | import { useArgs } from "@storybook/preview-api"; 5 | import { SelectOptionType } from "../../../src/Global.types"; 6 | import React from "react"; 7 | 8 | const meta = { 9 | title: "Components/Input/SelectGeneric", 10 | component: SelectGeneric, 11 | parameters: { 12 | layout: "centered", 13 | }, 14 | tags: ["autodocs"], 15 | argTypes: {}, 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | function Component(args: SelectGeneric) { 22 | const [, setArgs] = useArgs(); 23 | 24 | const onChange = (value: string) => { 25 | args.onChange?.(value); 26 | setArgs({ value }); 27 | }; 28 | return ; 29 | } 30 | 31 | const options: SelectOptionType[] = [ 32 | { value: "test1", label: "Label for test1" }, 33 | { value: "test2", label: "Label for test2" }, 34 | ]; 35 | 36 | export const Primary: Story = { 37 | args: { 38 | value: "", 39 | options: options, 40 | onChange: (event) => { 41 | console.log(event); 42 | }, 43 | }, 44 | render: Component, 45 | }; 46 | 47 | export const DefaultOptionLabel: Story = { 48 | args: { 49 | value: "", 50 | options: options, 51 | onChange: (event) => { 52 | console.log(event); 53 | }, 54 | defaultOptionValue: "My default option label", 55 | defaultOptionLabel: "My default option label", 56 | }, 57 | render: Component, 58 | }; 59 | 60 | export const DefaultOptionValue: Story = { 61 | args: { 62 | value: "", 63 | options: options, 64 | onChange: (event) => { 65 | console.log(event); 66 | }, 67 | defaultOptionValue: "My default option value", 68 | }, 69 | render: Component, 70 | }; 71 | 72 | export const WithoutDefaultOption: Story = { 73 | args: { 74 | value: "", 75 | options: options, 76 | onChange: (event) => { 77 | console.log(event); 78 | }, 79 | disableDefaultOption: true, 80 | }, 81 | render: Component, 82 | }; 83 | -------------------------------------------------------------------------------- /client/src/components/SelectGeneric.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | import { SelectOptionType } from "../../../src/Global.types"; 8 | 9 | import "./SelectGeneric.scss"; 10 | 11 | interface SelectGeneric { 12 | options: SelectOptionType[]; 13 | value: string; 14 | onChange: (value: string) => void; 15 | title?: string; 16 | className?: string; 17 | disableDefaultOption?: boolean; 18 | defaultOptionLabel?: string; 19 | defaultOptionValue?: string; 20 | disabled?: boolean; 21 | } 22 | 23 | const SelectGeneric = ({ 24 | options = [], 25 | value, 26 | title, 27 | className, 28 | onChange, 29 | disableDefaultOption = false, 30 | defaultOptionLabel = "None", 31 | defaultOptionValue = "", 32 | disabled = false, 33 | }: SelectGeneric) => { 34 | const intl = useIntl(); 35 | return ( 36 | 56 | ); 57 | }; 58 | 59 | export default SelectGeneric; 60 | -------------------------------------------------------------------------------- /client/src/components/ServiceMessage.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .ServiceMessage { 4 | position: fixed; 5 | top: 1rem; 6 | right: 1rem; 7 | height: auto; 8 | z-index: 100; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/components/ServiceMessage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useEffect, useState } from "react"; 7 | import "./ServiceMessage.scss"; 8 | import { Toast } from "./Toast"; 9 | import { useAppSelector } from "../app/hook"; 10 | import { INITIALIZED_TOAST } from "../../../src/Constants"; 11 | 12 | /** 13 | * Service Message for app 14 | * using Toast 15 | */ 16 | function ServiceMessage() { 17 | const serviceMessage = useAppSelector((state) => state.servicemessage); 18 | 19 | const [toast, setToast] = useState(INITIALIZED_TOAST); 20 | 21 | useEffect(() => { 22 | if (serviceMessage.toast.timestamp) setToast(serviceMessage.toast); 23 | }, [serviceMessage.toast.timestamp]); // eslint-disable-line react-hooks/exhaustive-deps 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | } 31 | 32 | export default ServiceMessage; 33 | -------------------------------------------------------------------------------- /client/src/components/Stepper.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .Stepper { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | row-gap: root.$rowgap-default; 8 | padding: root.$padding-default; 9 | .step { 10 | max-width: 4vw; 11 | align-self: center; 12 | justify-self: center; 13 | display: flex; 14 | flex-direction: column; 15 | row-gap: root.$rowgap-default; 16 | .separator { 17 | align-self: center; 18 | min-height: 3rem; 19 | width: 0.5rem; 20 | border-radius: root.$border-radius-default root.$border-radius-default 21 | root.$border-radius-round root.$border-radius-round; 22 | background-color: root.$stepper-background-color; 23 | box-shadow: root.$boxshadow-default; 24 | @media (prefers-color-scheme: dark) { 25 | background-color: root.$stepper-background-color-dark; 26 | box-shadow: root.$boxshadow-default-dark; 27 | } 28 | &.done { 29 | background-color: root.$color-success; 30 | } 31 | } 32 | .button { 33 | border-radius: root.$border-radius-round; 34 | border: 0; 35 | width: 3rem; 36 | height: 3rem; 37 | align-self: center; 38 | background-color: root.$color-text-secondary; 39 | box-shadow: root.$boxshadow-default; 40 | @media (prefers-color-scheme: dark) { 41 | box-shadow: root.$boxshadow-default-dark; 42 | } 43 | i { 44 | margin: auto; 45 | font-size: 1.5rem; 46 | } 47 | &.done { 48 | background-color: root.$color-success; 49 | color: root.$color-text-secondary; 50 | @media (prefers-color-scheme: dark) { 51 | color: root.$color-text-secondary-dark; 52 | } 53 | } 54 | &.undone { 55 | color: root.$color-text-primary; 56 | } 57 | &.active { 58 | background-color: root.$color-error; 59 | color: root.$color-text-secondary; 60 | @media (prefers-color-scheme: dark) { 61 | color: root.$color-text-secondary-dark; 62 | background-color: root.$color-error-dark; 63 | } 64 | } 65 | } 66 | label { 67 | font-weight: 500; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | width: 100%; 72 | } 73 | &.done { 74 | background-color: root.$color-success; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src/components/Stepper.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Stepper } from "./Stepper"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Components/Navigation/Stepper", 11 | component: Stepper, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | /** 27 | * Must be associated with the component StepperStep 28 | * controlled by parent 29 | * Here the first item is active so the associated StepperStep must be visible... 30 | */ 31 | export const Primary: Story = { 32 | args: { 33 | active: 0, 34 | steps: [ 35 | { 36 | label: "first step", 37 | }, 38 | { 39 | label: "second step", 40 | done: true, 41 | }, 42 | ], 43 | onChange: (changeDoneState: boolean, setNewActiveStep: number) => 44 | console.log(changeDoneState, setNewActiveStep), 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /client/src/components/Stepper.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import ButtonGeneric from "./ButtonGeneric"; 7 | 8 | import "./Stepper.scss"; 9 | import { useIntl } from "react-intl"; 10 | 11 | export type StepType = { 12 | label: string; 13 | done?: boolean; 14 | icon?: string; 15 | }; 16 | 17 | interface InternalStepProps { 18 | step: StepType; 19 | idx: number; 20 | } 21 | 22 | type SeparatorProps = { 23 | className?: string; 24 | }; 25 | interface StepperProps { 26 | steps: StepType[]; 27 | active: number; 28 | onChange: (changeDoneState: boolean, setNewActiveStep: number) => void; 29 | } 30 | export const Stepper = ({ steps = [], active, onChange }: StepperProps) => { 31 | const Separator = ({ className }: SeparatorProps) => { 32 | return
; 33 | }; 34 | 35 | const InternalStep = ({ step, idx }: InternalStepProps) => { 36 | const intl = useIntl(); 37 | 38 | return ( 39 |
40 | 41 | { 43 | onChange(false, idx); 44 | }} 45 | // if latest: special css 46 | icon={ 47 | step.done 48 | ? "check" 49 | : idx == steps.length - 1 50 | ? "eyeglass-2" 51 | : "arrows-minimize" 52 | } 53 | className={`button ${idx === active ? "active" : ""} ${ 54 | step.done ? "done" : "undone" 55 | }`} 56 | /> 57 | 60 |
61 | ); 62 | }; 63 | 64 | return ( 65 |
66 | {steps.map((item, idx) => { 67 | return ; 68 | })} 69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /client/src/components/StepperStep.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .StepperStep { 3 | width: 100%; 4 | margin-right: 1rem; 5 | &.notVisibleComponent { 6 | display: none; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/src/components/StepperStep.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { StepperStep } from "./StepperStep"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Components/Navigation/StepperStep", 11 | component: StepperStep, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | /** 27 | * Active because the property active === the property stepId 28 | * Controlled by parent 29 | */ 30 | export const Active: Story = { 31 | args: { 32 | active: 0, 33 | stepId: 0, 34 | children:
This is the component included in step of stepper
, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/components/StepperStep.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./StepperStep.scss"; 7 | 8 | interface StepperStepProps { 9 | children?: JSX.Element | string | JSX.Element[]; 10 | stepId: number; 11 | active: number; 12 | } 13 | export const StepperStep = ({ children, stepId, active }: StepperStepProps) => { 14 | return ( 15 |
20 | {children} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/components/Summary.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .Summary { 4 | display: flex; 5 | flex-direction: column; 6 | row-gap: root.$rowgap-default; 7 | padding: root.$padding-default; 8 | width: 100%; 9 | .Block { 10 | .Block { 11 | display: flex; 12 | flex-direction: column; 13 | h3 { 14 | width: 100%; 15 | } 16 | } 17 | &.details { 18 | flex-wrap: wrap; 19 | h2 { 20 | width: 100%; 21 | } 22 | } 23 | &.save { 24 | row-gap: root.$rowgap-default; 25 | column-gap: root.$columngap-default; 26 | align-items: center; 27 | justify-content: center; 28 | } 29 | &.identity, 30 | &.actions { 31 | flex-grow: 1; 32 | } 33 | .mustbesaved { 34 | animation-name: greenPulse; 35 | animation-duration: 3s; 36 | animation-iteration-count: infinite; 37 | color: root.$color-text-secondary; 38 | } 39 | } 40 | } 41 | @keyframes greenPulse { 42 | from { 43 | background-color: root.$sumury-keyframes-greenPulse-color-1; 44 | -webkit-box-shadow: 0 0 9px root.$sumury-keyframes-greenPulse-color-1; 45 | } 46 | 50% { 47 | background-color: root.$sumury-keyframes-greenPulse-color-2; 48 | -webkit-box-shadow: 0 0 10px root.$sumury-keyframes-greenPulse-color-2; 49 | } 50 | to { 51 | background-color: root.$sumury-keyframes-greenPulse-color-3; 52 | -webkit-box-shadow: 0 0 9px root.$sumury-keyframes-greenPulse-color-3; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/Summary.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Summary } from "./Summary"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { INITIALIZED_UPTODATEFORM } from "../../../src/Constants"; 9 | import { UptoDateOrNotState, UptodateForm } from "../../../src/Global.types"; 10 | 11 | const meta = { 12 | title: "Forms/Summary", 13 | component: Summary, 14 | decorators: [withRouter], 15 | parameters: { 16 | layout: "fullscreen", 17 | reactRouter: reactRouterParameters({ 18 | location: { path: "/" }, 19 | }), 20 | }, 21 | tags: ["autodocs"], 22 | argTypes: {}, 23 | } satisfies Meta; 24 | 25 | const activeUptodateForm: UptodateForm = { 26 | ...INITIALIZED_UPTODATEFORM, 27 | urlProduction: "https://test.com", 28 | scrapTypeProduction: "text", 29 | exprProduction: "version: (v[\\d.]+)", 30 | }; 31 | 32 | const compareResult: UptoDateOrNotState = { 33 | name: "xxxx", 34 | githubLatestRelease: "1.0.0", 35 | githubLatestReleaseIncludesProductionVersion: false, 36 | productionVersionIncludesGithubLatestRelease: false, 37 | productionVersion: "1.0.0", 38 | state: true, 39 | strictlyEqual: true, 40 | urlGitHub: "", 41 | urlProduction: "", 42 | }; 43 | 44 | export default meta; 45 | type Story = StoryObj; 46 | 47 | export const Primary: Story = { 48 | args: { 49 | uptodateForm: activeUptodateForm, 50 | isChangesOnModel: true, 51 | onSave: () => { 52 | return new Promise((resolv) => { 53 | resolv(null); 54 | }); 55 | }, 56 | onCompare: () => { 57 | return new Promise((resolv) => { 58 | resolv(compareResult); 59 | }); 60 | }, 61 | isRecordable: true, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /client/src/components/Toast.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | 3 | .containertoast { 4 | height: max-content; 5 | z-index: 9999; 6 | display: flex; 7 | flex-direction: column; 8 | row-gap: 0.2rem; 9 | max-width: 50vw; 10 | .toast { 11 | padding: 0; 12 | margin: 0; 13 | box-sizing: border-box; 14 | border: none; 15 | font-family: "Poppins", sans-serif; 16 | font-size: 14px; 17 | width: 100%; 18 | padding: 0; 19 | background-color: root.$background-color; 20 | border-radius: root.$border-radius-toast; 21 | display: flex; 22 | box-shadow: root.$boxshadow-toast; 23 | overflow: hidden; 24 | justify-content: space-between; 25 | @media (prefers-color-scheme: dark) { 26 | background-color: root.$background-color-dark; 27 | box-shadow: root.$boxshadow-toast-dark; 28 | border: 1px solid root.$palette-theme-origin-color-9; 29 | } 30 | 31 | p { 32 | margin: 0.3rem 0 0.3rem 0; 33 | } 34 | &.info { 35 | border-left: root.$toast-color-border-info; 36 | i { 37 | color: root.$toast-color-info; 38 | } 39 | } 40 | &.success { 41 | border-left: root.$toast-color-border-success; 42 | i { 43 | color: root.$toast-color-success; 44 | } 45 | } 46 | &.warn { 47 | border-left: root.$toast-color-border-warn; 48 | i { 49 | color: root.$toast-color-warn; 50 | } 51 | } 52 | &.error { 53 | border-left: root.$toast-color-border-error; 54 | i { 55 | color: root.$toast-color-error; 56 | } 57 | } 58 | .toast:not(:last-child) { 59 | margin-bottom: 50px; 60 | } 61 | .container-1, 62 | .container-2 { 63 | align-self: center; 64 | display: flex; 65 | flex-direction: column; 66 | } 67 | .container-1 { 68 | flex-grow: 0; 69 | } 70 | .container-2 { 71 | overflow: auto; 72 | .summary { 73 | color: root.$color-text-primary; 74 | font-weight: 600; 75 | font-size: 0.9rem; 76 | @media (prefers-color-scheme: dark) { 77 | color: root.$palette-theme-origin-color-6; 78 | } 79 | } 80 | .detail { 81 | font-size: 0.7rem; 82 | font-weight: 400; 83 | color: root.$toatst-color-detail; 84 | padding: root.$padding-default; 85 | overflow: auto; 86 | @media (prefers-color-scheme: dark) { 87 | color: root.$palette-theme-origin-color-6; 88 | } 89 | } 90 | } 91 | 92 | .container-1 i { 93 | font-size: 35px; 94 | } 95 | button { 96 | align-self: flex-start; 97 | background-color: transparent; 98 | font-size: 1.5rem; 99 | color: root.$toatst-color-button; 100 | line-height: 0; 101 | border: none; 102 | cursor: pointer; 103 | margin: 1rem 0.5rem 0 0.5rem; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/src/components/Toast.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Toast } from "./Toast"; 3 | import { INITIALIZED_TOAST } from "../../../src/Constants"; 4 | 5 | const meta = { 6 | title: "Components/Alert/Toast", 7 | component: Toast, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Info: Story = { 19 | args: { 20 | toast: { 21 | ...INITIALIZED_TOAST, 22 | summary: "severity info", 23 | detail: "details of alert", 24 | life: 1000000, 25 | }, 26 | }, 27 | }; 28 | 29 | export const Success: Story = { 30 | args: { 31 | toast: { 32 | ...INITIALIZED_TOAST, 33 | severity: "success", 34 | summary: "severity success", 35 | detail: "details of alert", 36 | life: 1000000, 37 | }, 38 | }, 39 | }; 40 | 41 | export const Warn: Story = { 42 | args: { 43 | toast: { 44 | ...INITIALIZED_TOAST, 45 | severity: "warn", 46 | summary: "severity warn", 47 | detail: "details of alert", 48 | life: 1000000, 49 | }, 50 | }, 51 | }; 52 | 53 | export const Error: Story = { 54 | args: { 55 | toast: { 56 | ...INITIALIZED_TOAST, 57 | severity: "error", 58 | summary: "severity error", 59 | detail: "details of alert", 60 | life: 5000, 61 | }, 62 | }, 63 | }; 64 | 65 | export const Sticky: Story = { 66 | args: { 67 | toast: { 68 | ...INITIALIZED_TOAST, 69 | severity: "error", 70 | summary: "severity error", 71 | detail: "details of alert", 72 | sticky: true, 73 | }, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /client/src/components/UrlLinkButtons.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .UrlLinkButtons { 3 | display: flex; 4 | width: 100%; 5 | flex-direction: row; 6 | align-items: center; 7 | justify-content: left; 8 | column-gap: root.$columngap-default; 9 | .ButtonGeneric { 10 | border-radius: root.$border-radius-round; 11 | } 12 | .urlhidden { 13 | display: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/UrlLinkButtons.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { UrlLinkButtons } from "./UrlLinkButtons"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "UI/UrlLinkButtons", 11 | component: UrlLinkButtons, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: { 28 | url: "https://github.com/dhenry123/utdon", 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/UrlLinkButtons.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | 8 | import "./UrlLinkButtons.scss"; 9 | import ButtonGeneric from "./ButtonGeneric"; 10 | import { convertUrlToTabName, copyToClipboard } from "../helpers/UiMiscHelper"; 11 | import { useRef } from "react"; 12 | import { useAppDispatch } from "../app/hook"; 13 | import { showServiceMessage } from "../app/serviceMessageSlice"; 14 | import { INITIALIZED_TOAST } from "../../../src/Constants"; 15 | 16 | interface UrlLinkButtonsProps { 17 | url: string; 18 | } 19 | export const UrlLinkButtons = ({ url }: UrlLinkButtonsProps) => { 20 | const intl = useIntl(); 21 | const dispatch = useAppDispatch(); 22 | const divRef = useRef(null); 23 | 24 | const openUrl = () => { 25 | window.open(url, convertUrlToTabName(url), "noopener,noreferrer"); 26 | }; 27 | const handleOnCopyToClipboard = async () => { 28 | copyToClipboard(divRef) 29 | .then(() => { 30 | dispatch( 31 | showServiceMessage({ 32 | ...INITIALIZED_TOAST, 33 | severity: "info", 34 | detail: intl.formatMessage({ 35 | id: "The url has been copied to Clipboard", 36 | }), 37 | }) 38 | ); 39 | }) 40 | .catch((error: Error) => { 41 | dispatch( 42 | showServiceMessage({ 43 | ...INITIALIZED_TOAST, 44 | severity: "error", 45 | detail: `Unexpected Error: ${error.toString()}`, 46 | }) 47 | ); 48 | }); 49 | }; 50 | return ( 51 |
52 | 57 | 62 |
63 | {url} 64 |
65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /client/src/components/UrlOpener.scss: -------------------------------------------------------------------------------- 1 | @use "../app/css/root.scss"; 2 | .UrlOpener { 3 | a { 4 | display: block; 5 | overflow: hidden; 6 | white-space: nowrap; 7 | text-overflow: ellipsis; 8 | max-width: 95%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/UrlOpener.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { UrlOpener } from "./UrlOpener"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "Components/Navigation/UrlOpener", 13 | component: UrlOpener, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: { url: "https://github.com/immich-app/immich" }, 46 | // if you need to get a specific render see SelectArs component... 47 | // render: (args) => Component(args), 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/components/UrlOpener.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./UrlOpener.scss"; 7 | 8 | interface UrlOpenerProp { 9 | url: string; 10 | className?: string; 11 | } 12 | export const UrlOpener = ({ url, className }: UrlOpenerProp) => { 13 | return ( 14 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/features/changepassword/ChangePassword.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | .ChangePassword { 3 | align-self: center; 4 | justify-self: center; 5 | width: 40vw; 6 | &.Block { 7 | display: flex; 8 | flex-direction: column; 9 | .groupButtons { 10 | margin: root.$margin-default; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | justify-content: center; 15 | column-gap: root.$columngap-default; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/features/changepassword/ChangePassword.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ChangePassword } from "./ChangePassword"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { fn } from "@storybook/test"; 9 | 10 | const meta = { 11 | title: "Features/ChangePassword", 12 | component: ChangePassword, 13 | decorators: [withRouter], 14 | parameters: { 15 | layout: "fullscreen", 16 | reactRouter: reactRouterParameters({ 17 | location: { path: "/" }, 18 | }), 19 | }, 20 | args: { 21 | onHide: fn(), 22 | }, 23 | tags: ["autodocs"], 24 | argTypes: {}, 25 | } satisfies Meta; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const Primary: Story = {}; 31 | -------------------------------------------------------------------------------- /client/src/features/controlmanagement/ControlManager.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | 3 | .ControlManager { 4 | display: flex; 5 | flex-direction: row; 6 | margin-top: root.$fixedheadermargintop; 7 | .Stepper { 8 | align-self: stretch; 9 | margin: root.$margin-default; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/features/controlmanagement/ControlManager.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { ControlManager } from "./ControlManager"; 4 | import { withRouter } from "storybook-addon-remix-react-router"; 5 | 6 | const meta = { 7 | title: "Features/ControlManager", 8 | component: ControlManager, 9 | decorators: [withRouter], 10 | parameters: { 11 | layout: "centered", 12 | }, 13 | tags: ["autodocs"], 14 | argTypes: {}, 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Primary: Story = { 21 | args: {}, 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/features/curlcommands/CurlCommands.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | .CurlCommands { 3 | display: flex; 4 | flex-direction: column; 5 | .listcurlcommands { 6 | overflow: auto; 7 | height: 60vh; 8 | } 9 | .name { 10 | div { 11 | font-weight: bold; 12 | color: root.$color-error; 13 | } 14 | } 15 | .closeButton { 16 | align-self: center; 17 | justify-self: center; 18 | padding: root.$padding-default; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/features/curlcommands/CurlCommands.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { CurlCommands } from "./CurlCommands"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | import { STORYBOOK_UPTODATEFORM } from "../../../../src/Constants-dev"; 9 | 10 | const meta = { 11 | title: "Features/CurlCommands", 12 | component: CurlCommands, 13 | decorators: [withRouter], 14 | parameters: { 15 | layout: "fullscreen", 16 | reactRouter: reactRouterParameters({ 17 | location: { path: "/" }, 18 | }), 19 | }, 20 | tags: ["autodocs"], 21 | argTypes: {}, 22 | } satisfies Meta; 23 | 24 | export default meta; 25 | type Story = StoryObj; 26 | 27 | export const ForOneControl: Story = { 28 | args: { 29 | uptodateForm: STORYBOOK_UPTODATEFORM, 30 | userAuthToken: "xxxxx", 31 | }, 32 | }; 33 | 34 | export const ForAllControls: Story = { 35 | args: { 36 | uptodateForm: "all", 37 | userAuthToken: "xxxxx", 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /client/src/features/displaycontrols/DisplayControls.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | .DisplayControls { 3 | display: flex; 4 | flex-direction: column; 5 | column-gap: 1rem; 6 | row-gap: 1rem; 7 | margin-top: root.$fixedheadermargintop; 8 | background-color: inherit; 9 | .list { 10 | display: flex; 11 | flex-wrap: wrap; 12 | column-gap: inherit; 13 | row-gap: inherit; 14 | justify-content: left; 15 | } 16 | .table-container { 17 | display: block; 18 | margin: 0.5em; 19 | width: 94%; 20 | 21 | .flex-table { 22 | display: grid; 23 | grid-template-columns: 3% 15% 18% 12% 18% 9% 9% 16%; 24 | grid-template-rows: 100% 100%; 25 | transition: 0.5s; 26 | box-shadow: root.$boxshadow-default; 27 | margin-bottom: 1rem; 28 | @media (prefers-color-scheme: dark) { 29 | box-shadow: root.$boxshadow-default-dark; 30 | } 31 | &:first-of-type .flex-row { 32 | color: root.$font-color-header; 33 | } 34 | &.flex-header { 35 | border-bottom: 1px solid root.$palette-theme-origin-color-10; 36 | margin-bottom: 4px; 37 | .flex-row { 38 | font-weight: bold; 39 | @media (prefers-color-scheme: dark) { 40 | color: root.$color-text-primary-dark; 41 | } 42 | } 43 | } 44 | } 45 | 46 | .flex-row { 47 | display: flex; 48 | width: 100%; 49 | text-align: left; 50 | padding: 0.5em 0.5em; 51 | height: 3.2rem; 52 | align-items: center; 53 | background: root.$background-color-controls-table; 54 | margin: 1px 0; 55 | border-radius: root.$border-radius-default; 56 | @media (prefers-color-scheme: dark) { 57 | background-color: root.$background-color-controls-table-dark; 58 | color: root.$color-text-primary-dark; 59 | } 60 | img { 61 | width: 2rem; 62 | height: 2rem; 63 | filter: grayscale(100%); 64 | } 65 | .ControlGroupButtons { 66 | margin-top: 4px; 67 | row-gap: 0.5rem; 68 | .groupButtons { 69 | justify-content: left; 70 | } 71 | } 72 | .urlProduction, 73 | .urlGitHub { 74 | max-width: 4rem; 75 | } 76 | .groups { 77 | display: block; 78 | overflow: hidden; 79 | white-space: nowrap; 80 | text-overflow: ellipsis; 81 | max-width: 95%; 82 | } 83 | &.state { 84 | cursor: pointer; 85 | } 86 | .fixedversion { 87 | display: flex; 88 | flex-direction: column; 89 | align-items: center; 90 | } 91 | .name { 92 | font-weight: bold; 93 | text-transform: capitalize; 94 | overflow: hidden; 95 | white-space: nowrap; 96 | text-overflow: ellipsis; 97 | max-width: 98%; 98 | } 99 | .uuid { 100 | font-style: italic; 101 | } 102 | } 103 | 104 | .rowspan { 105 | display: grid; 106 | grid-template-columns: 25% 75%; 107 | grid-template-rows: 100%; 108 | } 109 | 110 | .column { 111 | width: 100%; 112 | padding: 0; 113 | .flex-row { 114 | display: grid; 115 | grid-template-columns: repeat(auto-fill, 33.3%); 116 | grid-template-rows: 100% 100% 100%; 117 | width: 100%; 118 | padding: 0; 119 | border: 0; 120 | } 121 | } 122 | 123 | .flex-cell { 124 | text-align: center; 125 | padding: 0.5em 0.5em; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /client/src/features/errors/ErrorInRouter.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | 3 | .ErrorInRouter { 4 | color: root.$color-text-primary; 5 | margin: root.$margin-default; 6 | a, 7 | a:visited { 8 | color: root.$color-text-primary; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/src/features/errors/ErrorInRouter.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { 3 | reactRouterParameters, 4 | withRouter, 5 | } from "storybook-addon-remix-react-router"; 6 | import { ErrorInRouter } from "./ErrorInRouter"; 7 | 8 | const meta = { 9 | title: "Features/ErrorInRouter", 10 | component: ErrorInRouter, 11 | decorators: [withRouter], 12 | parameters: { 13 | layout: "centered", 14 | reactRouter: reactRouterParameters({ 15 | location: { path: "/test" }, 16 | }), 17 | }, 18 | tags: ["autodocs"], 19 | argTypes: {}, 20 | } satisfies Meta; 21 | 22 | export default meta; 23 | type Story = StoryObj; 24 | 25 | /** 26 | * Error Page when route not found 27 | */ 28 | export const RouteNotFound: Story = { 29 | args: {}, 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/features/errors/ErrorInRouter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useRouteError, ErrorResponse } from "react-router-dom"; 7 | import "./ErrorInRouter.scss"; 8 | import { useIntl } from "react-intl"; 9 | 10 | export const ErrorInRouter = () => { 11 | const intl = useIntl(); 12 | const error = useRouteError() as ErrorResponse; 13 | return ( 14 |
15 |

Oops!

16 |

17 | {" "} 18 | {intl.formatMessage({ id: "Sorry, an unexpected error has occurred" })}. 19 |

20 |

21 | 22 | {error.status} - {error.statusText} - {error.data} 23 | 24 |

25 | {intl.formatMessage({ id: "Return to the homepage" })} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/features/homepage/PageHome.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | .PageHome { 3 | background-color: inherit; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/features/homepage/PageHome.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { Header } from "../../components/Header"; 7 | import { Outlet } from "react-router-dom"; 8 | import "./PageHome.scss"; 9 | 10 | export const PageHome = () => { 11 | return ( 12 |
13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/features/login/PageLogin.scss: -------------------------------------------------------------------------------- 1 | @use "../../app/css/root.scss"; 2 | .PageLogin { 3 | margin-top: 0 !important; 4 | height: inherit; 5 | width: inherit; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | @media (prefers-color-scheme: dark) { 12 | color: root.$color-text-primary-dark; 13 | } 14 | 15 | .container { 16 | border: root.$border-default; 17 | padding: 2rem; 18 | .logo { 19 | width: 100%; 20 | flex-grow: 0; 21 | margin: root.$margin-default; 22 | } 23 | .login { 24 | display: flex; 25 | align-items: center; 26 | flex-grow: 1; 27 | .explanations { 28 | width: 50vw; 29 | background-image: url("/assets/download_image_1700775996997.png"); 30 | background-size: cover; 31 | background-repeat: no-repeat; 32 | width: 512px; 33 | height: 512px; 34 | display: flex; 35 | .title { 36 | font-size: 2.5rem; 37 | line-height: 7rem; 38 | padding: root.$padding-default; 39 | color: root.$color-text-primary; 40 | font-weight: bold; 41 | text-align: center; 42 | // transform: rotate(-10deg); /* Rotate the text by 45 degrees */ 43 | /* You may also want to adjust the transform-origin for positioning */ 44 | transform-origin: 0 0; /* Set the rotation origin to the top-left corner */ 45 | margin: 5rem 5rem; 46 | @media (prefers-color-scheme: dark) { 47 | color: root.$palette-theme-origin-color-26; 48 | } 49 | } 50 | } 51 | } 52 | @media (prefers-color-scheme: dark) { 53 | border: 1px solid root.$palette-theme-origin-color-29; 54 | } 55 | } 56 | .bottom { 57 | margin: root.$margin-default; 58 | } 59 | a:link { 60 | text-decoration: none !important; 61 | } 62 | a:visited { 63 | text-decoration: none !important; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/src/features/login/PageLogin.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { 4 | reactRouterParameters, 5 | withRouter, 6 | } from "storybook-addon-remix-react-router"; 7 | import { PageLogin } from "./PageLogin"; 8 | 9 | const meta = { 10 | title: "Features/PageLogin", 11 | component: PageLogin, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: {}, 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/features/login/PageLogin.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import "./PageLogin.scss"; 7 | 8 | import { useIntl } from "react-intl"; 9 | import { usePostUserLoginMutation } from "../../api/mytinydcUPDONApi"; 10 | import { clearToast, showServiceMessage } from "../../app/serviceMessageSlice"; 11 | import { useAppDispatch, useAppSelector } from "../../app/hook"; 12 | import { useNavigate } from "react-router-dom"; 13 | import { LoginBlock } from "../../components/LoginBlock"; 14 | import { PostAuthent } from "../../../../src/Global.types"; 15 | import { 16 | APPLICATION_VERSION, 17 | INITIALIZED_TOAST, 18 | } from "../../../../src/Constants"; 19 | import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; 20 | 21 | export const PageLogin = () => { 22 | const dispatch = useAppDispatch(); 23 | const intl = useIntl(); 24 | 25 | const navigate = useNavigate(); 26 | const [userLogin] = usePostUserLoginMutation(); 27 | 28 | const applicationContext = useAppSelector( 29 | (state) => state.context.application 30 | ); 31 | /** 32 | * @param {string} jsonloginpassword - see swagger documentation (data model) 33 | */ 34 | const handleOnLogin = async (jsonloginpassword: PostAuthent) => { 35 | return await userLogin(jsonloginpassword) 36 | .unwrap() 37 | .then(async () => { 38 | // reset Toast 39 | dispatch(clearToast()); 40 | return navigate("/"); 41 | }) 42 | .catch((error: FetchBaseQueryError) => { 43 | let message = intl.formatMessage({ id: "Authentication failure" }); 44 | if (error.status !== 401) 45 | message = intl.formatMessage({ 46 | id: "Unexpected error, see server logs", 47 | }); 48 | dispatch( 49 | showServiceMessage({ 50 | ...INITIALIZED_TOAST, 51 | detail: intl.formatMessage({ id: message }), 52 | }) 53 | ); 54 | return null; 55 | }); 56 | }; 57 | 58 | return ( 59 |
60 |
61 |
62 |
63 |
64 | 70 | Is Your FOSS Application UpToDateOrNot? 71 | 72 |
73 | 74 |
75 |
76 | © Copyright{" "} 77 | 78 | Mytinydc.com 79 | 80 | {" - "} {applicationContext.copyright} - Licence{" "} 81 | {applicationContext.licence} 82 | {" - "} 83 | 84 | {intl.formatMessage({ id: "API Documentation" })} 85 | 86 | {` - Version: ${APPLICATION_VERSION}`} 87 |
88 |
89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /client/src/features/usermanager/UserManager.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { UserManager } from "./UserManager.tsx"; 4 | import { 5 | reactRouterParameters, 6 | withRouter, 7 | } from "storybook-addon-remix-react-router"; 8 | 9 | const meta = { 10 | title: "Features/UserManager", 11 | component: UserManager, 12 | decorators: [withRouter], 13 | parameters: { 14 | layout: "fullscreen", 15 | reactRouter: reactRouterParameters({ 16 | location: { path: "/" }, 17 | }), 18 | }, 19 | tags: ["autodocs"], 20 | argTypes: {}, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const Primary: Story = { 27 | args: {}, 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/helpers/DateHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { IntlShape } from "react-intl"; 7 | 8 | export const getRelativeTime = (ts: number, intl: IntlShape) => { 9 | const formatter = new Intl.RelativeTimeFormat(intl.locale, { 10 | style: `long`, 11 | }); 12 | let period: Intl.RelativeTimeFormatUnit = "day"; 13 | let diff = (new Date(ts).valueOf() - new Date().valueOf()) / 1000 / 86400; 14 | if (Math.abs(diff) < 1) { 15 | period = "hour"; 16 | diff = (new Date(ts).valueOf() - new Date().valueOf()) / 1000 / 3600; 17 | if (Math.abs(diff) < 1) { 18 | period = "minute"; 19 | diff = (new Date(ts).valueOf() - new Date().valueOf()) / 1000 / 60; 20 | } 21 | } 22 | return `${intl.formatMessage({ 23 | id: "Execution date", 24 | })}: ${new Date(ts).toLocaleDateString()} ${new Date( 25 | ts 26 | ).toLocaleTimeString()} (${formatter.format(Math.trunc(diff), period)})`; 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/helpers/ExprSamples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { SelectOptionType } from "../../../src/Global.types"; 7 | 8 | export const jmespathProductionSamples: SelectOptionType[] = [ 9 | { 10 | value: "version", 11 | label: "One of the properties (eg: version)", 12 | }, 13 | { 14 | value: "api.version", 15 | label: "A property of property (eg: api.version)", 16 | }, 17 | { 18 | value: "join('.',*)", 19 | label: "Gather all properties", 20 | }, 21 | { 22 | value: "{prefix: 'v',test:join('.',*)}|join('',*)", 23 | label: "Gather all properties & prefix with v", 24 | }, 25 | { 26 | value: "{prefix: 'V',test:join('.',*)}|join('',*)", 27 | label: "Gather all properties & prefix with V", 28 | }, 29 | { 30 | value: "{prefix: 'Version',test:join('.',*)}|join('',*)", 31 | label: "Gather all properties & prefix with Version", 32 | }, 33 | ]; 34 | 35 | export const regExprProductionSamples: SelectOptionType[] = [ 36 | { 37 | value: "(v[\\d+\\.+]+)", 38 | label: "v[x.] repetead (eg: vx, vx.y, vx.y.x...)", 39 | }, 40 | { 41 | value: "(v[\\d+]\\.[\\d+]\\.[\\d+])", 42 | label: "vx.y.z strict", 43 | }, 44 | { 45 | value: "(v[\\d+]\\.[\\d+])", 46 | label: "vx.y strict", 47 | }, 48 | { 49 | value: "(V[\\d+]\\.[\\d+]\\.[\\d+])", 50 | label: "Vx.y.z strict", 51 | }, 52 | { 53 | value: "(V[\\d+]\\.[\\d+])", 54 | label: "Vx.y strict", 55 | }, 56 | { 57 | value: "([v|V][\\d+]\\.[\\d+]\\.[\\d+])", 58 | label: "vx.y.z | Vx.y.z strict", 59 | }, 60 | { 61 | value: "([v|V][\\d+]\\.[\\d+])", 62 | label: "vx.y | Vx.y strict", 63 | }, 64 | 65 | { 66 | value: "([\\d+]\\.[\\d+]\\.[\\d+])", 67 | label: "x.y.z strict", 68 | }, 69 | { 70 | value: "([\\d+]\\.[\\d+])", 71 | label: "x.y strict", 72 | }, 73 | ]; 74 | 75 | export const regExprGithubSamples: SelectOptionType[] = [ 76 | { value: "^[v|V][0-9\\.]+$", label: "Keep vx.y & vx.y.z & Vx.y & Vx.y.z" }, 77 | { value: "^[v|V]([0-9.]+$)", label: "Exclude prefix and keep x.y & x.y.z" }, 78 | { value: "^v[0-9\\.]+$", label: "Keep vx.y & vx.y.z" }, 79 | { value: "^V[0-9\\.]+$", label: "Keep Vx.y & Vx.y.z" }, 80 | { value: "^v[0-9]+\\.[0-9]+$", label: "Keep vx.y" }, 81 | { value: "^V[0-9]+\\.[0-9]+$", label: "Keep Vx.y" }, 82 | { value: "^[0-9\\.]+$", label: "Keep x.y & x.y.z" }, 83 | { value: "^[0-9]+\\.[0-9]+\\.[0-9]+\\.$", label: "Keep x.y.z" }, 84 | { value: "^[0-9]+\\.[0-9]+$", label: "Keep x.y" }, 85 | { 86 | value: "(^v1.[0-9]+.[0-9]+$)", 87 | label: "Keep only v1.x.y", 88 | }, 89 | { value: ".*", label: "All" }, 90 | ]; 91 | -------------------------------------------------------------------------------- /client/src/helpers/UiMiscHelper.ts: -------------------------------------------------------------------------------- 1 | import { Option } from "react-multi-select-component"; 2 | /** 3 | * generic method to copy div content to clipboard 4 | * @param divRef 5 | * @returns 6 | */ 7 | export const copyToClipboard = (divRef: React.MutableRefObject) => { 8 | return new Promise((resolv, reject) => { 9 | if (divRef.current) { 10 | try { 11 | const textToCopy = (divRef.current as HTMLDivElement).innerText; 12 | const textarea = document.createElement("textarea"); 13 | textarea.value = textToCopy; 14 | document.body.appendChild(textarea); 15 | textarea.select(); 16 | document.execCommand("copy"); 17 | document.body.removeChild(textarea); 18 | resolv(null); 19 | } catch (error) { 20 | reject(error); 21 | } 22 | } else { 23 | reject(new Error("divRef must be provided")); 24 | } 25 | }); 26 | }; 27 | 28 | export const buidMultiSelectGroups = (groups: string[]): Option[] => { 29 | const options: Option[] = []; 30 | for (const group of groups) { 31 | options.push({ label: group, value: group }); 32 | } 33 | return options; 34 | }; 35 | 36 | export const convertUrlToTabName = (url: string) => { 37 | return url 38 | .replace( 39 | /^(?:https?:\/\/)?(?:[^@/\n]+@)?(?:www\.)?([^:/?\n]+\.+[^:/?\n]+)/, 40 | "$1" 41 | ) 42 | .replace(/\.+/g, "_"); 43 | }; 44 | -------------------------------------------------------------------------------- /client/src/helpers/rtk.ts: -------------------------------------------------------------------------------- 1 | import { scrapUrlHeaderType } from "../../../src/Global.types"; 2 | 3 | export const buildHeader = (value: string): scrapUrlHeaderType => { 4 | return { scrapUrlHeader: value }; 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/helpers/scrapUrl.ts: -------------------------------------------------------------------------------- 1 | import { HTTPMethods } from "../../../src/Global.types"; 2 | 3 | export const scrapUrl = async ( 4 | url: string, 5 | method: HTTPMethods = "GET", 6 | customHttpHeader?: string 7 | ): Promise => { 8 | let authHeader: RequestInit = { method: method }; 9 | const header = new Headers(); 10 | if (customHttpHeader) { 11 | const split = customHttpHeader.split(":"); 12 | if (split[1]) { 13 | header.append(split[0], split[1]); 14 | authHeader = { ...authHeader, headers: header }; 15 | } 16 | } 17 | 18 | const content = await fetch(`${url}`, { ...authHeader }).then( 19 | async (response) => { 20 | if (!response.ok) { 21 | const message = `An error has occured: ${response.status}`; 22 | throw new Error(message); 23 | } 24 | // WARNING: (anti XSS) Content must never be interpreted by the browser 25 | const text = await response.text(); 26 | return text; 27 | } 28 | ); 29 | return content; 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom/client"; 8 | import { App } from "./App.tsx"; 9 | import { store } from "./app/store"; 10 | import { Provider } from "react-redux"; 11 | 12 | ReactDOM.createRoot(document.getElementById("root")!).render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/tools/addComponents.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # @author DHENRY for mytinydc.com 4 | # @license AGPL3 5 | 6 | # ask 7 | # - Composant Name 8 | 9 | pathComponent="src/components" 10 | pathTemplate="tools/tplComponents" 11 | 12 | echo "****************************************" 13 | echo "* Tool : Create New Component" 14 | echo "****************************************" 15 | echo "" 16 | read -p " [INFO] Provide the name of new component: " component 17 | echo "" 18 | 19 | if [ "$component" == "" ]; then 20 | echo "[ERROR] You have to provide new component name" 21 | exit 1 22 | fi 23 | 24 | files="TPL.scss TPL.stories.tsx TPL.tsx" 25 | 26 | for file in $files; do 27 | finalFileName=$(echo $file | sed -E "s/TPL/$component/") 28 | if [ -f "$pathComponent/$finalFileName" ]; then 29 | echo "[WARN] $pathComponent/$finalFileName already exists" 30 | continue 31 | fi 32 | echo Copying "$pathTemplate/$file to $pathComponent/$finalFileName" 33 | cp "$pathTemplate/$file" "$pathComponent/$finalFileName" 34 | if [ -f "$pathComponent/$finalFileName" ]; then 35 | sed -E -i "s/TPL/$component/g" "$pathComponent/$finalFileName" 36 | else 37 | echo "[ERROR] File $pathComponent/$finalFileName doesn't exist, impossible to continue" 38 | exit 1 39 | fi 40 | done 41 | -------------------------------------------------------------------------------- /client/tools/tplComponents/TPL.scss: -------------------------------------------------------------------------------- 1 | @use "root.scss"; 2 | .TPL { 3 | } 4 | -------------------------------------------------------------------------------- /client/tools/tplComponents/TPL.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | // import { useArgs } from "@storybook/preview-api"; 3 | 4 | import { TPL } from "./TPL"; 5 | import { 6 | reactRouterParameters, 7 | withRouter, 8 | } from "storybook-addon-remix-react-router"; 9 | 10 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 11 | const meta = { 12 | title: "NewComponent/TPL", 13 | component: TPL, 14 | decorators: [withRouter], 15 | parameters: { 16 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout 17 | layout: "fullscreen", 18 | reactRouter: reactRouterParameters({ 19 | location: { path: "/" }, 20 | }), 21 | }, 22 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs 23 | tags: ["autodocs"], 24 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 25 | argTypes: {}, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | // If you need to keep state in storybook, you also could use the app redux store 32 | // const Component = ({ ...args }) => { 33 | // const [, setArgs] = useArgs(); 34 | // const onChange = (value: string) => { 35 | // // Call the provided callback 36 | // // This is used for the Actions tab 37 | // args.onChange?.(value); 38 | // 39 | // // Update the arg in Storybook 40 | // setArgs({ value }); 41 | // }; 42 | // return ; 43 | // }; 44 | export const Primary: Story = { 45 | args: {}, 46 | // if you need to get a specific render see SelectArs component... 47 | // render: (args) => Component(args), 48 | }; 49 | -------------------------------------------------------------------------------- /client/tools/tplComponents/TPL.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { useIntl } from "react-intl"; 7 | import { useAppDispatch } from "../app/hook"; 8 | import { useNavigate } from "react-router-dom"; 9 | 10 | import "./TPL.scss"; 11 | export const TPL = () => { 12 | const intl = useIntl(); 13 | const dispatch = useAppDispatch(); 14 | const navigate = useNavigate(); 15 | 16 | return
; 17 | }; 18 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "exclude": ["node_modules", "dist", "**/*.stories.tsx"] 25 | } 26 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: "0.0.0.0", 9 | port: 7852, 10 | open: false, 11 | proxy: { 12 | "/api": { 13 | //Vite default cors server option is : accept all 14 | target: "http://0.0.0.0:3015", 15 | secure: false, 16 | }, 17 | }, 18 | }, 19 | build: { 20 | manifest: true, 21 | }, 22 | css: { 23 | preprocessorOptions: { 24 | scss: { 25 | api: "modern-compiler", 26 | silenceDeprecations: ["legacy-js-api"], 27 | }, 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/data/.gitkeep -------------------------------------------------------------------------------- /doc/GROUPS.md: -------------------------------------------------------------------------------- 1 | # Gestion des utilisateurs/groupes 2 | 3 | Introduit dans la version 1.4, les utilisateurs peuvent être gérés par groupes, et chaque contrôles peut-être affecté **à un ou plusieurs groupes**. 4 | 5 | ## Création des groupes 6 | 7 | Se connecter en administrateur, puis ouvrir le module de "gestion des utilisateurs". 8 | 9 | ![utilisateurs et groupes](./assets/Screenshot_usersgroupsmanager.png) 10 | 11 | Selectionner ou ajouter un utilisateur, dans la zone de recherche des "Groupes", taper le nom du nouveau groupe , le composant vous proposera de créer le groupe. 12 | 13 | ![Ajout de groupes](./assets/Screenshot_addGroup.png) 14 | 15 | Envoyer les modifications, **le ou les groupes** sont créés. 16 | 17 | ## Affectation d'un contrôle aux groupes 18 | 19 | **Pour réduire la compléxité**, la gestion "fine" des contrôles n'a pas été implémentée (read/write/execute). 20 | 21 | Quand un contrôle est affecté à un groupe, chaque utilisateur du groupe devient "gestionnaire" de ce contrôle. Il peut par conséquent le modifier ou le supprimer. 22 | 23 | ![Affectation de groupes à un contrôle](./assets/Screenshot_setGroupsControl.png) 24 | 25 | ## Affectation automatique des groupes 26 | 27 | Lorsque qu'un **administrateur** crée un contrôle, **il doit affecter** un ou plusieurs groupes ayant autorité sur ce dernier. 28 | 29 | Lorsque qu'un utilisateur **non administrateur** crée un contrôle, il est automatiquement affecté à tous ses groupes, libre à lui de réduire cette sélection. 30 | 31 | **Un contrôle ne peut être enregistré que** s'il dispose d'au moins un groupe. 32 | -------------------------------------------------------------------------------- /doc/assets/Screenshot_addGroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/doc/assets/Screenshot_addGroup.png -------------------------------------------------------------------------------- /doc/assets/Screenshot_setGroupsControl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/doc/assets/Screenshot_setGroupsControl.png -------------------------------------------------------------------------------- /doc/assets/Screenshot_usersgroupsmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/doc/assets/Screenshot_usersgroupsmanager.png -------------------------------------------------------------------------------- /doc/assets/utdon-dashboard-mytinydc.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/doc/assets/utdon-dashboard-mytinydc.com.png -------------------------------------------------------------------------------- /doc/en/GROUPS.md: -------------------------------------------------------------------------------- 1 | # User/group management 2 | 3 | Introduced in version 1.4, users can be managed in groups, and each control can be assigned **to one or more groups**. 4 | 5 | ## Création des groupes 6 | 7 | Log on as administrator, then open the "user management" module. 8 | 9 | ![users and groups](../assets/Screenshot_usersgroupsmanager.png) 10 | 11 | Select or add a user, in the "Groups" search box, type the name of the new group, the component will prompt you to create the group. 12 | 13 | ![Add groups](../assets/Screenshot_addGroup.png) 14 | 15 | Send the changes, **the group or groups** are created. 16 | 17 | ## Assigning control to groups 18 | 19 | **To reduce complexity**, fine-grained control management has not been implemented (read/write/execute). 20 | 21 | When a control is assigned to a group, each user in the group becomes the "manager" of that control. They can therefore modify or delete it. 22 | 23 | ![Assigning groups to a control](../assets/Screenshot_setGroupsControl.png) 24 | 25 | ## Automatic group assignment 26 | 27 | When an **administrator** creates a control, **he/she must assign** one or more groups with authority over it. 28 | 29 | When a **non-administrator** user creates a control, it is automatically assigned to all its groups, but they are free to reduce this selection. 30 | 31 | **A control can only be registered** if it has at least one group. 32 | -------------------------------------------------------------------------------- /install-legacy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | installdir="/usr/local/utdon" 3 | service="utdon" 4 | # Custom settings 5 | NODE_EXTRA_CA_CERTS="" 6 | NODE_TLS_REJECT_UNAUTHORIZED="" 7 | HTTP_PROXY="" 8 | HTTPS_PROXY="" 9 | PROXYCA_CERT="" 10 | 11 | # NodeJS is needed 12 | which node >/dev/null 2>&1 13 | if [ "$?" == "1" ]; then 14 | echo "You have to install nodejs package (==LTS-20)" 15 | exit 1 16 | fi 17 | 18 | # Build application 19 | npm install && npm run build 20 | cd client && npm install && npm run build && cd .. 21 | rm -r node_modules && npm install --omit=dev 22 | 23 | sudo mkdir -p $installdir/public $installdir/data 24 | sudo cp -R dist/* $installdir/. 25 | sudo cp ./openapi.yaml $installdir/. 26 | sudo cp -R client/dist/* $installdir/public/. 27 | sudo cp -R node_modules $installdir/. 28 | rm -r node_modules dist client/node_modules client/dist 29 | 30 | # Service installation 31 | echo "################" 32 | echo "" 33 | echo "UTDON has been built and installed in the directory '$installdir'" 34 | echo "" 35 | echo "- Create user/group: $service/$service" 36 | echo "" 37 | echo "- Then 'chown $service:$service $installdir/data" 38 | echo "" 39 | echo "- Generate two secrets" 40 | USER_ENCRYPT_SECRET="$(openssl rand -base64 32)" 41 | echo "USER_ENCRYPT_SECRET=$USER_ENCRYPT_SECRET" 42 | DATABASE_ENCRYPT_SECRET="$(openssl rand -base64 32)" 43 | echo "DATABASE_ENCRYPT_SECRET=$DATABASE_ENCRYPT_SECRET" 44 | echo "" 45 | echo "*** Keep both secrets safe.... ***" 46 | echo "" 47 | echo "- Create service in /etc/systemd/system/$service.service" 48 | echo "" 49 | cat < { 21 | let uptodateState = false, 22 | githubLatestReleaseIncludesProductionVersion = false, 23 | productionVersionIncludesGithubLatestRelease = false, 24 | strictlyEqual = false; 25 | // strict equality 26 | if (sourceCodeVersion === productionVersion) { 27 | uptodateState = true; 28 | strictlyEqual = true; 29 | } else { 30 | if (sourceCodeVersion.match(productionVersion)) { 31 | githubLatestReleaseIncludesProductionVersion = true; 32 | uptodateState = true; 33 | } 34 | if (productionVersion.match(sourceCodeVersion)) { 35 | productionVersionIncludesGithubLatestRelease = true; 36 | uptodateState = true; 37 | } 38 | } 39 | return { 40 | name: name, 41 | githubLatestRelease: sourceCodeVersion, 42 | productionVersion: productionVersion, 43 | state: uptodateState, 44 | strictlyEqual: strictlyEqual, 45 | githubLatestReleaseIncludesProductionVersion: 46 | githubLatestReleaseIncludesProductionVersion, 47 | productionVersionIncludesGithubLatestRelease: 48 | productionVersionIncludesGithubLatestRelease, 49 | urlGitHub: urlGitHub, 50 | urlProduction: urlProduction, 51 | ts: new Date().valueOf(), 52 | }; 53 | }; 54 | 55 | /** 56 | * order by outofdate, uptodatewithwarning, uptodate 57 | * @param rec 58 | * @returns 59 | */ 60 | export const recordsOrder = (rec: UptodateForm[]) => { 61 | const toUpdate: UptodateForm[] = []; 62 | const upTodateWithWarning: UptodateForm[] = []; 63 | const upTodate: UptodateForm[] = []; 64 | for (const item of rec) { 65 | if (item.compareResult && item.compareResult.ts) { 66 | if (item.compareResult.state && item.compareResult.strictlyEqual) { 67 | upTodate.push(item); 68 | continue; 69 | } else if ( 70 | item.compareResult.state && 71 | !item.compareResult.strictlyEqual 72 | ) { 73 | upTodateWithWarning.push(item); 74 | continue; 75 | } 76 | } 77 | toUpdate.push(item); 78 | } 79 | return toUpdate.concat(upTodateWithWarning).concat(upTodate); 80 | }; 81 | -------------------------------------------------------------------------------- /src/lib/GlobalGithubToken.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { Authentification } from "./Authentification"; 3 | import { UptodateForm } from "../Global.types"; 4 | 5 | const getFilePath = (): string => { 6 | const ggtDbPathDev = `${__dirname}/../../data//globalGithubToken`; 7 | const ggtDbPath = `${__dirname}/../data/globalGithubToken`; 8 | 9 | return process.env.environment === "development" ? ggtDbPathDev : ggtDbPath; 10 | }; 11 | 12 | export const setGlobalGithubToken = (token: string) => { 13 | writeFileSync( 14 | getFilePath(), 15 | Authentification.dataEncrypt(token, process.env.DATABASE_ENCRYPT_SECRET), 16 | "utf-8" 17 | ); 18 | return; 19 | }; 20 | 21 | export const getGlobalGithubToken = (): string => { 22 | const tokenEncrypted: string = readFileSync(getFilePath(), "utf-8"); 23 | if (tokenEncrypted && tokenEncrypted.trim() !== "") { 24 | return Authentification.dataDecrypt( 25 | tokenEncrypted, 26 | process.env.DATABASE_ENCRYPT_SECRET 27 | ); 28 | } 29 | return ""; 30 | }; 31 | 32 | // raw authorization header [key]:[value] 33 | export const setControlGlobalGithubToken = ( 34 | record: UptodateForm, 35 | globalGithubToken: string 36 | ): UptodateForm => { 37 | // header already set on control 38 | if (record.headerkeyGit && record.headervalueGit) return record; 39 | if (globalGithubToken && /github\.com/.test(record.urlGitHub)) { 40 | // global github token is provided, set header attributes values 41 | return { 42 | ...record, 43 | headerkeyGit: "Authorization", 44 | headervalueGit: `Bearer ${globalGithubToken}`, // see github document authentication 45 | authGlobale: true, 46 | }; 47 | } 48 | return record; 49 | }; 50 | 51 | export const getHeaderGlobalGithubToken = ( 52 | url: string, 53 | header: string, 54 | globalGithubToken: string 55 | ): string => { 56 | // header already set no change 57 | if (header) return header; 58 | // github url & globalGithubToken set using global authentication 59 | if (globalGithubToken && /github\.com/.test(url)) 60 | return `Authorization:Bearer ${globalGithubToken}`; // see github document authentication 61 | // no header 62 | return ""; 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/PatchVersion.ts: -------------------------------------------------------------------------------- 1 | import { UptodateForm } from "../Global.types"; 2 | 3 | // V1.3.0 -> V1.4.0 4 | // Add groups attribut to controls 5 | export const patchV1_3_0To1_4_0 = async (db: UptodateForm[]) => { 6 | const newDb: UptodateForm[] = []; 7 | for (const record of db) { 8 | // if groups not defined set groups to admin 9 | if (record.groups === undefined) { 10 | newDb.push({ ...record, groups: ["admin"] }); 11 | } else { 12 | newDb.push({ ...record }); 13 | } 14 | } 15 | return newDb; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/helperGitRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { 7 | GiteaReleaseTagModel, 8 | GithubReleaseTagModel, 9 | TypeGitRepo, 10 | } from "../Global.types"; 11 | 12 | export const getGitUrlTagReleases = ( 13 | gitRepoUrl: string, 14 | typeRepo: TypeGitRepo 15 | ) => { 16 | if (typeRepo === "github") { 17 | const githubApiReleasesEntry = "https://api.github.com/repos"; 18 | const regExpExtractDomain = "^https?:\\/\\/[^@\\/\n]+\\/"; 19 | const owner = gitRepoUrl.replace(new RegExp(regExpExtractDomain), ""); 20 | return `${githubApiReleasesEntry}/${owner}/tags`; 21 | } else { 22 | //Gitea other solution 23 | //xxxxDOMAINxxx/api/v1/repos/xxxOWNERxxx/releases 24 | const regExp = "(^https?:\\/\\/[^@\\/\n]+\\/)(.*)"; 25 | return gitRepoUrl.replace(new RegExp(regExp), "$1api/v1/repos/$2/releases"); 26 | } 27 | }; 28 | 29 | /** 30 | * common server && UI 31 | * @param filtersName 32 | * @param filtered 33 | * @returns 34 | */ 35 | export const filterAndReplace = ( 36 | filtersName: string, 37 | filtered: string[] 38 | ): string => { 39 | if (filtersName && filtersName.match(/\(/)) { 40 | return filtered[0].replace(new RegExp(filtersName), "$1"); 41 | } else { 42 | return filtered[0]; 43 | } 44 | }; 45 | 46 | /** 47 | * As there are only 2 possibilities: check url 48 | * @param url 49 | */ 50 | export const getTypeGitRepo = (url: string): TypeGitRepo => { 51 | return /github\.com/.test(url) ? "github" : "gitea"; 52 | }; 53 | 54 | export const getLatestRelease = ( 55 | typeRepo: TypeGitRepo, 56 | releaseTags: string, 57 | filtersName?: string 58 | ): string => { 59 | const json = JSON.parse(releaseTags as string); 60 | if (json && Array.isArray(json)) { 61 | const filtered: string[] = json 62 | .filter((item) => { 63 | const tag = getTagFromGitRepoResponse( 64 | typeRepo, 65 | item as GiteaReleaseTagModel | GithubReleaseTagModel 66 | ); 67 | return filtersName ? tag.match(filtersName) : tag; 68 | }) 69 | .map((item) => { 70 | return getTagFromGitRepoResponse( 71 | typeRepo, 72 | item as GiteaReleaseTagModel | GithubReleaseTagModel 73 | ) as string; 74 | }); 75 | // tags and filter to apply 76 | if (filtered.length > 0 && filtersName) { 77 | return filterAndReplace(filtersName, filtered); 78 | } else if (filtered.length > 0 && !filtersName) { 79 | //tags and no filter return the first 80 | return filtered[0]; 81 | } 82 | } 83 | // if Github change specifications ???? - hard to test 84 | // trying with an other domain return 404 85 | return ""; 86 | }; 87 | 88 | export const getTagFromGitRepoResponse = ( 89 | typeRepo: TypeGitRepo, 90 | data: GithubReleaseTagModel | GiteaReleaseTagModel 91 | ): string => { 92 | return typeRepo === "gitea" 93 | ? (data.tag_name as string) 94 | : (data.name as string); 95 | }; 96 | -------------------------------------------------------------------------------- /src/lib/helperProdVersionReader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { search } from "@metrichor/jmespath"; 7 | 8 | /** 9 | * extract data from text by regexp match & regexp subst - only group[1] is return 10 | * @param targetText 11 | * @param regexpMatch 12 | * @returns 13 | */ 14 | export const filterText = ( 15 | targetText: string | null, 16 | regexpMatch: string 17 | ): string => { 18 | try { 19 | if (!regexpMatch || !regexpMatch.trim()) return ""; 20 | if (targetText) { 21 | const match = targetText.match(new RegExp(regexpMatch)); 22 | if (match && match[1]) return match[1]; 23 | } 24 | return ""; 25 | } catch (error) { 26 | return ""; 27 | } 28 | }; 29 | 30 | /** 31 | * extract data from json by key 32 | * @param outputJson 33 | * @param key 34 | * @returns 35 | */ 36 | export const filterJson = (outputJson: string | null, expr: string): string => { 37 | if (outputJson) { 38 | try { 39 | // search join allowed on strings onlyconvert all number TYPE attributs to String : https://stackoverflow.com/a/7389888 40 | const json = outputJson.replace(/: *(\d+)([ *, *\\}])/g, ':"$1"$2'); 41 | // all number are string so search is usable 42 | if (json && expr) { 43 | const res = search(JSON.parse(json), expr); 44 | if (res) return JSON.stringify(res).replace(/^"+|"+$/g, ""); 45 | } 46 | } catch (error) { 47 | // unexpected errors 48 | return ""; 49 | } 50 | } 51 | return ""; 52 | }; 53 | 54 | /** 55 | * is String parsable as JSON 56 | * @param data 57 | * @returns 58 | */ 59 | export const isJsonParsable = (data: string | null) => { 60 | if (!data) return false; 61 | try { 62 | JSON.parse(data); 63 | } catch (e) { 64 | return false; 65 | } 66 | return true; 67 | }; 68 | -------------------------------------------------------------------------------- /src/lib/logs.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import { logError, logInfo, OptionsLogType } from "../Global.types"; 3 | import { SessionExt } from "../ServerTypes"; 4 | 5 | const getBase = (req: Request): logInfo => { 6 | const session = req.session as SessionExt; 7 | return { 8 | userId: session.user?.uuid ? session.user.uuid : "uuid unknown", 9 | userLogin: session.user?.login 10 | ? session.user.login 11 | : "session user unknown", 12 | ipAddr: req.ip || "unknown", 13 | apiPath: req.path, 14 | apiMethod: req.method, 15 | }; 16 | }; 17 | export const getLogObjectInfo = ( 18 | req: Request, 19 | optionsLog?: OptionsLogType 20 | ): logInfo => { 21 | let logItem: logInfo = getBase(req); 22 | if (optionsLog) { 23 | if (optionsLog.message) 24 | logItem = { ...logItem, message: optionsLog.message }; 25 | if (optionsLog.uuid) logItem = { ...logItem, message: optionsLog.uuid }; 26 | if (optionsLog.scrapResponse) 27 | logItem = { ...logItem, message: optionsLog.scrapResponse }; 28 | 29 | if (optionsLog.gitAuthenticationProvided) 30 | logItem = { 31 | ...logItem, 32 | gitAuthenticationProvided: optionsLog.gitAuthenticationProvided, 33 | }; 34 | if (optionsLog.productionAuthenticationProvided) 35 | logItem = { 36 | ...logItem, 37 | productionAuthenticationProvided: 38 | optionsLog.productionAuthenticationProvided, 39 | }; 40 | if (optionsLog.newUser) 41 | logItem = { ...logItem, newUser: optionsLog.newUser }; 42 | if (optionsLog.userDeleted) 43 | logItem = { ...logItem, userDeleted: optionsLog.userDeleted }; 44 | if (optionsLog.userUpdated) 45 | logItem = { ...logItem, userUpdated: optionsLog.userUpdated }; 46 | if (optionsLog.userLogout) 47 | logItem = { ...logItem, userUpdated: optionsLog.userLogout }; 48 | } 49 | return logItem; 50 | }; 51 | 52 | export const getLogObjectError = ( 53 | req: Request, 54 | error: string, 55 | optionsLog?: OptionsLogType 56 | ): logError => { 57 | let logItem: logInfo = getBase(req); 58 | if (optionsLog) { 59 | if (optionsLog.message) 60 | logItem = { ...logItem, message: optionsLog.message }; 61 | if (optionsLog.uuid) logItem = { ...logItem, message: optionsLog.uuid }; 62 | if (optionsLog.scrapResponse) 63 | logItem = { ...logItem, message: optionsLog.scrapResponse }; 64 | } 65 | return { ...logItem, error: error }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/routes/routerCore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import express, { NextFunction, Request, Response } from "express"; 7 | import { APPLICATION_VERSION } from "../Constants"; 8 | import { 9 | getGlobalGithubToken, 10 | getHeaderGlobalGithubToken, 11 | } from "../lib/GlobalGithubToken"; 12 | import { scrapUrlThroughProxy } from "../lib/scrapUrlServer"; 13 | import { InfosScrapConnection } from "../Global.types"; 14 | const routerCore = express.Router(); 15 | 16 | /** 17 | * Used to retrieve url content from the production server, github API tags... 18 | */ 19 | routerCore.get( 20 | "/scrap/:url", 21 | async (req: Request, res: Response, next: NextFunction) => { 22 | if (req.params.url !== undefined) { 23 | const header = getHeaderGlobalGithubToken( 24 | req.params.url, 25 | req.headers.scrapurlheader as string, 26 | getGlobalGithubToken() 27 | ); 28 | await scrapUrlThroughProxy( 29 | req.params.url, 30 | "GET", 31 | header, 32 | process.env.HTTP_PROXY, 33 | process.env.HTTPS_PROXY 34 | ) 35 | .then((data: InfosScrapConnection) => { 36 | // Warn Reduxtoolkit expect text so data will always be type = string 37 | res.status(200).send(data.data); 38 | }) 39 | .catch((error: Error) => { 40 | res 41 | .status(500) 42 | .json({ error: `${error.toString()}-${req.params.url}` }); 43 | }); 44 | } else { 45 | next(new Error("Url is not provided")); 46 | } 47 | } 48 | ); 49 | 50 | /** 51 | * To get the application version 52 | */ 53 | routerCore.get( 54 | "/version", 55 | (req: Request, res: Response, next: NextFunction) => { 56 | try { 57 | res.status(200).send({ version: APPLICATION_VERSION }); 58 | } catch (error: unknown) { 59 | next(error); 60 | } 61 | } 62 | ); 63 | 64 | /** 65 | * Could be used to find out if the service is healthy 66 | */ 67 | routerCore.get( 68 | "/healthz", 69 | (req: Request, res: Response, next: NextFunction) => { 70 | try { 71 | res.status(204).send(); 72 | } catch (error: unknown) { 73 | next(error); 74 | } 75 | } 76 | ); 77 | 78 | /** 79 | * metrics not implemented 80 | */ 81 | routerCore.get( 82 | "/metrics", 83 | async (req: Request, res: Response, next: NextFunction) => { 84 | try { 85 | res.status(503).json("Service is not available"); 86 | } catch (error: unknown) { 87 | next(error); 88 | } 89 | } 90 | ); 91 | 92 | export default routerCore; 93 | -------------------------------------------------------------------------------- /test/Features.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author DHENRY for mytinydc.com 3 | * @license AGPL3 4 | */ 5 | 6 | import { readFileSync } from "fs"; 7 | import { compareVersion, recordsOrder } from "../src/lib/Features"; 8 | import { UptodateForm } from "../src/Global.types"; 9 | 10 | describe("Features", () => { 11 | describe("compareVersion", () => { 12 | test("compareVersion - strictlyEqual true", () => { 13 | const name = "xxxxx"; 14 | const result = compareVersion(name, "x100", "x100", "", ""); 15 | expect(result.name).toEqual(name); 16 | expect(result.githubLatestRelease).toEqual("x100"); 17 | expect(result.productionVersion).toEqual("x100"); 18 | expect(result.strictlyEqual).toBeTruthy(); 19 | expect(result.productionVersionIncludesGithubLatestRelease).toBeFalsy(); 20 | expect(result.githubLatestReleaseIncludesProductionVersion).toBeFalsy(); 21 | expect(result.state).toBeTruthy(); 22 | expect(result.ts).toBeGreaterThan(0); 23 | }); 24 | 25 | test("compareVersion - githubLatestReleaseIncludesProductionVersion", () => { 26 | const name = "xxxxx"; 27 | const result = compareVersion(name, "x100", "x10", "", ""); 28 | expect(result.name).toEqual(name); 29 | expect(result.githubLatestRelease).toEqual("x100"); 30 | expect(result.productionVersion).toEqual("x10"); 31 | expect(result.strictlyEqual).toBeFalsy(); 32 | expect(result.productionVersionIncludesGithubLatestRelease).toBeFalsy(); 33 | expect(result.githubLatestReleaseIncludesProductionVersion).toBeTruthy(); 34 | expect(result.state).toBeTruthy(); 35 | expect(result.ts).toBeGreaterThan(0); 36 | }); 37 | 38 | test("compareVersion - productionVersionIncludesGithubLatestRelease", () => { 39 | const name = "xxxxx"; 40 | const result = compareVersion(name, "100", "x100", "", ""); 41 | expect(result.name).toEqual(name); 42 | expect(result.githubLatestRelease).toEqual("100"); 43 | expect(result.productionVersion).toEqual("x100"); 44 | expect(result.strictlyEqual).toBeFalsy(); 45 | expect(result.productionVersionIncludesGithubLatestRelease).toBeTruthy(); 46 | expect(result.githubLatestReleaseIncludesProductionVersion).toBeFalsy(); 47 | expect(result.state).toBeTruthy(); 48 | expect(result.ts).toBeGreaterThan(0); 49 | }); 50 | }); 51 | 52 | describe("recordsOrder", () => { 53 | const json = JSON.parse( 54 | readFileSync(`${process.cwd()}/test/samples/reorder1.json`, "utf-8") 55 | ) as UptodateForm[]; 56 | const origOrder = json.map((item) => item.name); 57 | const json1 = JSON.parse( 58 | readFileSync(`${process.cwd()}/test/samples/reorder2.json`, "utf-8") 59 | ) as UptodateForm[]; 60 | const origOrder1 = json.map((item) => item.name); 61 | test("recordsOrder - Nothing to do", () => { 62 | const result = recordsOrder(json); 63 | const orderedNames = result.map((item) => item.name); 64 | // console.log(origOrder); 65 | // console.log(orderedNames); 66 | expect(origOrder).toEqual(orderedNames); 67 | }); 68 | test("recordsOrder - Reorder", () => { 69 | const result = recordsOrder(json1); 70 | const orderedNames = result.map((item) => item.name); 71 | // console.log(origOrder1); 72 | // console.log(orderedNames); 73 | expect(origOrder1).not.toEqual(orderedNames); 74 | expect(orderedNames[0]).toEqual("immich"); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/cacerts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/test/cacerts/.keep -------------------------------------------------------------------------------- /test/checkJestInstallation/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from "./sum"; 2 | 3 | test("adds 1 + 2 to equal 3", () => { 4 | expect(sum(1, 2)).toBe(3); 5 | }); 6 | -------------------------------------------------------------------------------- /test/checkJestInstallation/sum.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => { 2 | return a + b; 3 | }; 4 | -------------------------------------------------------------------------------- /test/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/test/data/.gitkeep -------------------------------------------------------------------------------- /test/samples/database-empty.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /test/samples/database-malformed-object.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/samples/database-nocontent.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhenry123/utdon/a62a05b8b0a1633b072d198e28770b09c8077b42/test/samples/database-nocontent.json -------------------------------------------------------------------------------- /test/samples/database-readonly.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "xxxxxx", 4 | "urlProduction": "https://xxxxxxxx", 5 | "scrapTypeProduction": "json", 6 | "exprProduction": "join('.',*)", 7 | "urlGitHub": "https://xxxxxxxxx", 8 | "exprGithub": "v[\\d+.]+", 9 | "urlCronJobMonitoring": "https://xxxxxxxxxxxxx", 10 | "urlCronJobMonitoringAuth": "xxxxxxxx", 11 | "urlCICD": "https://xxxxxxxxxxxxxxxxx", 12 | "urlCICDAuth": "xxxxxxxx", 13 | "uuid": "54ca6573-35db-4bde-ad0e-fcac93b53883", 14 | "isPause": false, 15 | "compareResult": null 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /test/samples/database.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "xxxxxx", 4 | "urlProduction": "https://xxxxxxxx", 5 | "scrapTypeProduction": "json", 6 | "exprProduction": "join('.',*)", 7 | "urlGitHub": "https://xxxxxxxxx", 8 | "exprGithub": "v[\\d+.]+", 9 | "urlCronJobMonitoring": "https://xxxxxxxxxxxxx", 10 | "urlCronJobMonitoringAuth": "xxxxxxxx", 11 | "urlCICD": "https://xxxxxxxxxxxxxxxxx", 12 | "urlCICDAuth": "xxxxxxxx", 13 | "uuid": "d05aeb69-205f-49b5-af5c-23120505843c", 14 | "isPause": false, 15 | "compareResult": null 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /test/samples/html-version.response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/samples/json-version.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "version": "v3.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/samples/reorder1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "codeberg", 4 | "compareResult": { 5 | "state": true, 6 | "strictlyEqual": false, 7 | "ts": 1739798558315 8 | } 9 | }, 10 | { 11 | "name": "Test avec proxy", 12 | "compareResult": { 13 | "state": true, 14 | "strictlyEqual": false, 15 | "ts": 1739798500090 16 | } 17 | }, 18 | { 19 | "name": "immich", 20 | "compareResult": { 21 | "state": true, 22 | "strictlyEqual": true, 23 | "ts": 1739798557982 24 | } 25 | }, 26 | { 27 | "name": "pgadmin4", 28 | "compareResult": { 29 | "state": true, 30 | "strictlyEqual": true, 31 | "ts": 1739798559013 32 | } 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /test/samples/reorder2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "codeberg", 4 | "compareResult": { 5 | "state": true, 6 | "strictlyEqual": false, 7 | "ts": 1739798558315 8 | } 9 | }, 10 | { 11 | "name": "Test avec proxy", 12 | "compareResult": { 13 | "state": true, 14 | "strictlyEqual": false, 15 | "ts": 1739798500090 16 | } 17 | }, 18 | { 19 | "name": "immich", 20 | "compareResult": { 21 | "state": false, 22 | "strictlyEqual": true, 23 | "ts": 1739798557982 24 | } 25 | }, 26 | { 27 | "name": "pgadmin4", 28 | "compareResult": { 29 | "state": true, 30 | "strictlyEqual": true, 31 | "ts": 1739798559013 32 | } 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /test/samples/users-before-PR#15.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "admin", 3 | "uuid": "xxxxxxx", 4 | "password": "xxxxx", 5 | "bearer": "xxx" 6 | } 7 | -------------------------------------------------------------------------------- /updateVersion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Increment version 3 | if [ "$1" != "major" ] && [ "$1" != "minor" ] && [ "$1" != "patch" ]; then 4 | echo "You have to provide, the kind of increment" 5 | echo "major|minor|patch" 6 | exit 1 7 | fi 8 | 9 | read -p "Do you confirm version update (y/N) ? " confirm 10 | 11 | if [ "$confirm" == "y" ]; then 12 | # package.json server && UI 13 | npm version --no-git-tag-version "$1" && cd client && npm version --no-git-tag-version "$1" && cd .. 14 | newversion=$(jq '.version' package.json | sed -E 's/"//g') 15 | # constants file 16 | constFile="src/Constants.ts" 17 | sed -i "s/APPLICATION_VERSION.*$/APPLICATION_VERSION = \"$newversion\";/" "$constFile" 18 | grep APPLICATION_VERSION "$constFile" 19 | fi 20 | --------------------------------------------------------------------------------