diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs index e3610d9..2337e7e 100644 --- a/.commitlintrc.mjs +++ b/.commitlintrc.mjs @@ -1,4 +1,4 @@ -const config = { +export default { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ @@ -19,12 +19,8 @@ const config = { 'ui', 'wip', 'publish', - 'docker', - 'WIP', ], ], }, ignores: [(message) => message.includes('WIP'), (message) => message.includes('wip')], }; - -export default config; diff --git a/.env.sample b/.env.sample index a2e1c7c..68d3bf0 100644 --- a/.env.sample +++ b/.env.sample @@ -23,6 +23,8 @@ CONFIG_PATH=./config SSH_PATH=./ssh SSH_HOST=./ssh_host BORG_REPOSITORY_PATH=./repos +TMP_PATH=./tmp +LOGS_PATH=./logs ## Optional variables section ## diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 136334e..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Bug report -about: Create a report a bug -title: '' -labels: '' -assignees: '' - ---- - -**BorgWarehouse version :** -**Installation type :** -- [ ] Docker -- [ ] Baremetal (Debian/Ubuntu) -- [ ] Other environment : - -------- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Additional context** -Add any other context about the problem here. - -**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/) - is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.** diff --git a/.github/ISSUE_TEMPLATE/i-need-help.md b/.github/ISSUE_TEMPLATE/i-need-help.md deleted file mode 100644 index 8aa78e6..0000000 --- a/.github/ISSUE_TEMPLATE/i-need-help.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: I need help -about: You need help about installation, usage, or specific cases. -title: '' -labels: help wanted -assignees: '' - ---- - -**BorgWarehouse version :** -**Installation type :** -- [ ] Docker -- [ ] Baremetal (Debian/Ubuntu) -- [ ] Other environment : - -------- - -Describe your problem here. - -**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/) - is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34a7437..3110836 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,16 @@ version: 2 updates: - - package-ecosystem: 'docker' - directory: '/' + - package-ecosystem: "docker" + directory: "/" schedule: - interval: 'daily' - # Note: Dependabot uses "npm" ecosystem but automatically detects pnpm-lock.yaml - # Make sure package-lock.json is gitignored to prevent confusion - - package-ecosystem: 'npm' - directory: '/' + interval: "daily" + - package-ecosystem: "npm" + directory: "/" schedule: - interval: 'daily' + interval: "daily" # Maintain dependencies for GitHub Actions # src: https://github.com/marketplace/actions/build-and-push-docker-images#keep-up-to-date-with-github-dependabot - - package-ecosystem: 'github-actions' - directory: '/' + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: 'daily' + interval: "daily" diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml index 825fd0d..cbc945a 100644 --- a/.github/workflows/bats.yml +++ b/.github/workflows/bats.yml @@ -1,8 +1,5 @@ name: Bats -permissions: - contents: read - on: push: branches: @@ -19,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/docker-image-develop.yml b/.github/workflows/docker-image-develop.yml index 14c33a5..509c1ec 100644 --- a/.github/workflows/docker-image-develop.yml +++ b/.github/workflows/docker-image-develop.yml @@ -5,15 +5,12 @@ on: branches: - 'develop' -permissions: - contents: read - jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Update package.json version run: | COMMIT=$(git rev-parse --short HEAD) diff --git a/.github/workflows/docker-image-latest.yml b/.github/workflows/docker-image-latest.yml index 7957cb8..f3a4554 100644 --- a/.github/workflows/docker-image-latest.yml +++ b/.github/workflows/docker-image-latest.yml @@ -1,6 +1,4 @@ name: Build and Push Docker Image -permissions: - contents: read on: push: @@ -12,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx diff --git a/.github/workflows/docker-image-release.yml b/.github/workflows/docker-image-release.yml index 01f4eb5..eabb0d2 100644 --- a/.github/workflows/docker-image-release.yml +++ b/.github/workflows/docker-image-release.yml @@ -5,15 +5,12 @@ on: types: - published -permissions: - contents: read - jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx diff --git a/.github/workflows/docker-image-test.yml b/.github/workflows/docker-image-test.yml index 4d716ad..55e4ad1 100644 --- a/.github/workflows/docker-image-test.yml +++ b/.github/workflows/docker-image-test.yml @@ -1,8 +1,5 @@ name: Test to build docker container on Pull Request -permissions: - contents: read - on: pull_request: branches: @@ -14,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 8b954af..92522ae 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -4,21 +4,19 @@ on: - main - develop pull_request: - branches: - - main - - develop + branches: main -name: 'Shellcheck' +name: "Shellcheck" permissions: {} jobs: shellcheck: name: Shellcheck runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v6 - + - uses: actions/checkout@v4 + - name: Run ShellCheck uses: ludeeus/action-shellcheck@master env: diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml deleted file mode 100644 index fe9d512..0000000 --- a/.github/workflows/vitest.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Vitest & ESLint CI - -on: - push: - branches: - - main - - develop - pull_request: - branches: - - main - - develop - -permissions: - contents: read - -jobs: - test: - name: Run Vitest - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run Vitest - run: pnpm run test - - lint: - name: Run ESLint - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run ESLint - run: pnpm exec eslint diff --git a/.gitignore b/.gitignore index c95738d..53f9ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -50,14 +50,6 @@ typings/ # Optional npm cache directory .npm -# pnpm -.pnpm-store/ -pnpm-debug.log* - -# Lock files (pnpm-lock.yaml is used) -package-lock.json -yarn.lock - # Optional eslint cache .eslintcache diff --git a/.husky/append-icon.sh b/.husky/append-icon.sh index a54594a..8990b55 100755 --- a/.husky/append-icon.sh +++ b/.husky/append-icon.sh @@ -23,43 +23,41 @@ function checkBreakingChangeInBody() { } function findTypeIcon() { + # get message from 1st param message="$1" - if [[ "$message" =~ ^.*!:\ .* ]]; then - echo "$boomIcon" - return 0 - fi + # declare an icons for each authorized enum-type from `.commitlintrc.js` + declare -A icons + icons[build]='πŸ€–' + icons[chore]='🧹' + icons["chore(deps)"]='🧹' + icons[config]='πŸ”§' + icons[deploy]='πŸš€' + icons[doc]='πŸ“š' + icons[feat]='✨' + icons[fix]='πŸ›' + icons[hotfix]='πŸš‘' + icons[i18n]='πŸ’¬' + icons[publish]='πŸ“¦' + icons[refactor]='⚑' + icons[revert]='βͺ' + icons[test]='βœ…' + icons[ui]='🎨' + icons[wip]='🚧' + icons[WIP]='🚧' - declare -A icons=( - [build]='πŸ€–' - [chore]='🧹' - ["chore(deps)"]='🧹' - [config]='πŸ”§' - [deploy]='πŸš€' - [doc]='πŸ“š' - [feat]='✨' - [fix]='πŸ›' - [hotfix]='πŸš‘' - [i18n]='πŸ’¬' - [publish]='πŸ“¦' - [refactor]='⚑' - [revert]='βͺ' - [test]='βœ…' - [ui]='🎨' - [wip]='🚧' - [WIP]='🚧' - [docker]='🐳' - ) - - commit_type="${message%%:*}" - - icon="${icons[$commit_type]}" - if [[ -n "$icon" ]]; then - echo "$icon" - return 0 - else - return 1 - fi + for type in "${!icons[@]}"; do + # check if message subject contains breaking change pattern + if [[ "$message" =~ ^(.*)(!:){1}(.*)$ ]]; then + echo "$boomIcon" + return 0 + # else find corresponding type icon + elif [[ "$message" == "$type"* ]]; then + echo "${icons[$type]}" + return 0 + fi + done + return 1 } # extract original message from the first line of file diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cbcc181..0000000 --- a/.npmrc +++ /dev/null @@ -1,7 +0,0 @@ -# Configuration pnpm -auto-install-peers=true -strict-peer-dependencies=false -shamefully-hoist=false - -# Force pnpm usage (prevent npm/yarn) -package-manager=pnpm diff --git a/Components/Repo/QuickCommands/QuickCommands.tsx b/Components/Repo/QuickCommands/QuickCommands.js similarity index 72% rename from Components/Repo/QuickCommands/QuickCommands.tsx rename to Components/Repo/QuickCommands/QuickCommands.js index 8fe57b3..d32002e 100644 --- a/Components/Repo/QuickCommands/QuickCommands.tsx +++ b/Components/Repo/QuickCommands/QuickCommands.js @@ -1,30 +1,26 @@ +//Lib import React from 'react'; import { useState } from 'react'; import classes from './QuickCommands.module.css'; import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react'; -import { lanCommandOption } from '~/helpers/functions'; -import { WizardEnvType } from '~/types/domain/config.types'; +import lanCommandOption from '../../../helpers/functions/lanCommandOption'; -type QuickCommandsProps = { - repositoryName: string; - wizardEnv?: WizardEnvType; - lanCommand?: boolean; -}; - -export default function QuickCommands(props: QuickCommandsProps) { +export default function QuickCommands(props) { + ////Vars const wizardEnv = props.wizardEnv; //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled. const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.lanCommand); + //State const [isCopied, setIsCopied] = useState(false); + //Functions const handleCopy = async () => { // Asynchronously call copy to clipboard navigator.clipboard - .writeText( - `ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./${props.repositoryName}` - ) + .writeText(`ssh://${wizardEnv.UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.repositoryName}`) .then(() => { + // If successful, update the isCopied state value setIsCopied(true); setTimeout(() => { setIsCopied(false); @@ -41,8 +37,8 @@ export default function QuickCommands(props: QuickCommandsProps) {
Copied !
) : (
- ssh://{wizardEnv?.UNIX_USER}@{FQDN} - {SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./ + ssh://{wizardEnv.UNIX_USER}@{FQDN} + {SSH_SERVER_PORT}/./ {props.repositoryName}
)} diff --git a/Components/Repo/QuickCommands/QuickCommands.module.css b/Components/Repo/QuickCommands/QuickCommands.module.css index 77cc329..faba43d 100644 --- a/Components/Repo/QuickCommands/QuickCommands.module.css +++ b/Components/Repo/QuickCommands/QuickCommands.module.css @@ -2,7 +2,7 @@ display: flex; align-items: center; align-self: flex-start; - margin: auto 25px auto auto; + margin: auto 47px auto auto; } .icons { @@ -31,7 +31,7 @@ width: 100%; height: 100%; border: 1px solid #6d4aff21; - background-color: #fafafa; + background-color: #f5f5f5; border-radius: 5px; box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; color: #65748b; @@ -50,7 +50,6 @@ .copyValid { margin: auto 8px auto auto; - padding: 6px 6px; font-size: 0.95rem; color: #6d4aff; animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; @@ -77,7 +76,7 @@ width: 100%; height: 100%; border: 1px solid #6d4aff21; - background-color: #fafafa; + background-color: #f5f5f5; border-radius: 5px; box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; color: #65748b; diff --git a/Components/Repo/Repo.js b/Components/Repo/Repo.js new file mode 100644 index 0000000..1e2312a --- /dev/null +++ b/Components/Repo/Repo.js @@ -0,0 +1,180 @@ +//Lib +import { useState } from 'react'; +import classes from './Repo.module.css'; +import { + IconSettings, + IconInfoCircle, + IconChevronDown, + IconChevronUp, + IconBellOff, + IconLockPlus, +} from '@tabler/icons-react'; +import timestampConverter from '../../helpers/functions/timestampConverter'; +import StorageBar from '../UI/StorageBar/StorageBar'; +import QuickCommands from './QuickCommands/QuickCommands'; + +export default function Repo(props) { + //Load displayDetails from LocalStorage + const displayDetailsFromLS = () => { + try { + if (localStorage.getItem('displayDetailsRepo' + props.id) === null) { + localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(true)); + return true; + } else { + return JSON.parse(localStorage.getItem('displayDetailsRepo' + props.id)); + } + } catch (error) { + console.log( + 'LocalStorage error, key', + 'displayDetailsRepo' + props.id, + 'will be removed. Try again.', + 'Error message on this key : ', + error + ); + localStorage.removeItem('displayDetailsRepo' + props.id); + } + }; + + //States + const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS); + + //BUTTON : Display or not repo details for ONE repo + const displayDetailsForOneHandler = (boolean) => { + //Update localStorage + localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(boolean)); + setDisplayDetails(boolean); + }; + + //Status indicator + const statusIndicator = () => { + return props.status ? classes.statusIndicatorGreen : classes.statusIndicatorRed; + }; + + //Alert indicator + const alertIndicator = () => { + if (props.alert === 0) { + return ( +
+ +
+ ); + } + }; + + const appendOnlyModeIndicator = () => { + if (props.appendOnlyMode) { + return ( +
+ +
+ ); + } + }; + + return ( + <> + {displayDetails ? ( + <> +
+
+
+
{props.alias}
+ {appendOnlyModeIndicator()} + {alertIndicator()} + {props.comment && ( +
+ +
{props.comment}
+
+ )} + +
+ + + + + + + + + + + + + + + + + + + + + + +
RepositoryStorage SizeStorage UsedLast changeIDEdit
{props.repositoryName}{props.storageSize} GB + + +
+ {props.lastSave === 0 ? '-' : timestampConverter(props.lastSave)} +
+
#{props.id} +
+ props.repoManageEditHandler()} + /> +
+
+
+ + ) : ( + <> +
+
+
+
{props.alias}
+ {appendOnlyModeIndicator()} + {alertIndicator()} + {props.comment && ( +
+ +
{props.comment}
+
+ )} +
+
+ {props.lastSave === 0 ? null : timestampConverter(props.lastSave)} + #{props.id} +
+
+ + )} + {displayDetails ? ( +
+ { + displayDetailsForOneHandler(false); + }} + /> +
+ ) : ( +
+ { + displayDetailsForOneHandler(true); + }} + /> +
+ )} + + ); +} diff --git a/Components/Repo/Repo.module.css b/Components/Repo/Repo.module.css index 8311137..d2f52fe 100644 --- a/Components/Repo/Repo.module.css +++ b/Components/Repo/Repo.module.css @@ -13,39 +13,16 @@ overflow: visible; /* Need to display comment on hover (which is position : absolute) */ position: relative; - background: #fff; } .closeFlex { display: flex; align-items: center; - justify-content: space-between; - width: 100%; padding: 15px; - gap: 10px; } .RepoClose .lastSave { - white-space: nowrap; -} - -.RepoClose .leftGroup { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - gap: 10px; -} - -.RepoClose .alias { - font-weight: bold; - color: #111827; - font-size: 1.05em; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 1; - min-width: 0; + padding: 15px; } /* REPO OPEN */ @@ -58,6 +35,7 @@ 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); width: auto; + max-height: 200px; margin: 20px 0px 0px 0px; padding: 15px; border-radius: 5px; @@ -65,28 +43,14 @@ overflow: visible; /* Need to display comment on hover (which is position : absolute) */ position: relative; - background: #fff; } .openFlex { - display: block; - width: 100%; -} - -.aliasFlex { display: flex; - flex-direction: row; align-items: center; + align-self: flex-start; width: 100%; } -.indicatorsFlex { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; - width: 100%; - gap: 15px; -} .tabInfo { width: 100%; @@ -95,7 +59,7 @@ background: #fff; border-radius: 10px; overflow: hidden; - margin: 15px auto; + margin: 25px auto; table-layout: fixed; } @@ -109,7 +73,7 @@ font-size: 1em; color: #fff; line-height: 1.2; - font-weight: 500; + font-weight: normal; } .tabInfo tbody tr { @@ -124,52 +88,80 @@ } /*STATUS*/ -.statusIndicatorGreen, -.statusIndicatorRed { - border-radius: 50%; - height: 16px; - width: 16px; - flex-shrink: 0; - animation: pulse 5s infinite; -} .statusIndicatorGreen { - background: #00d26a; - box-shadow: 0 0 0 0 rgba(0, 210, 106, 0.7); + background: rgb(9, 255, 0); + border-radius: 50%; + margin: 10px; + height: 15px; + width: 15px; + box-shadow: 0 0 0 0 rgb(9, 255, 0); + transform: scale(1); + animation: pulseGreen 5s infinite; + animation-delay: 1s; +} + +@keyframes pulseGreen { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(17, 255, 0, 0.7); + } + + 10% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(17, 255, 0, 0); + } + + 90% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(17, 255, 0, 0); + } } .statusIndicatorRed { - background: #ff3d3d; - box-shadow: 0 0 0 0 rgba(255, 61, 61, 0.7); + background: rgb(255, 0, 0); + border-radius: 50%; + margin: 10px; + height: 15px; + width: 15px; + + box-shadow: 0 0 0 0 rgb(255, 0, 0); + transform: scale(1); + animation: pulseRed 5s infinite; animation-delay: 0.5s; } -@keyframes pulse { +@keyframes pulseRed { 0% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4); + box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); } + 10% { transform: scale(1); - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); } + 90% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); } } /* Alert icon */ + .alertIcon { display: flex; flex-direction: row; align-items: center; + margin-left: 10px; } .appendOnlyModeIcon { display: flex; flex-direction: row; align-items: center; + margin-left: 10px; } /* GENERAL */ @@ -177,13 +169,6 @@ font-weight: bold; color: #111827; font-size: 1.05em; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.RepoOpen .alias { - margin-top: 5px; } .lastSave { @@ -199,6 +184,7 @@ display: flex; flex-direction: row; align-items: center; + margin-left: 10px; } .toolTip { @@ -241,69 +227,23 @@ /* MOBILE */ @media all and (max-width: 1000px) { - .openFlex, - .tabInfo, - .toolTip, - .comment, - .chevron { - display: none !important; + .tabInfo { + display: none; } - - .RepoOpen, - .RepoClose { - display: flex !important; - flex-direction: row !important; - align-items: center !important; - justify-content: space-between !important; - max-height: 65px !important; - padding: 15px !important; - margin: 20px 0 0 0 !important; + .toolTip { + display: none; } - - .closeFlex { - display: flex !important; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 0 !important; - margin: 0 !important; + .comment { + display: none; } - - .alias { - flex: 1; - min-width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .leftGroup { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - gap: 10px; - } - - .rightGroup { - display: flex; - align-items: center; - gap: 10px; - flex-shrink: 0; - } - .lastSave { - display: block !important; - color: #65748b; - white-space: nowrap; - font-size: 0.85em; - flex-shrink: 0; - margin-left: 10px; + display: none; } - - .appendOnlyModeIcon, - .alertIcon { - display: flex; - align-items: center; + .closeFlex { + margin: auto; + } + .openFlex { + margin: auto; + width: auto; } } diff --git a/Components/Repo/Repo.tsx b/Components/Repo/Repo.tsx deleted file mode 100644 index a659670..0000000 --- a/Components/Repo/Repo.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { useState, useMemo } from 'react'; -import classes from './Repo.module.css'; -import { - IconSettings, - IconInfoCircle, - IconChevronDown, - IconChevronUp, - IconBellOff, - IconLockPlus, -} from '@tabler/icons-react'; -import StorageBar from '../UI/StorageBar/StorageBar'; -import QuickCommands from './QuickCommands/QuickCommands'; -import { Repository, WizardEnvType, Optional } from '~/types'; -import { fromUnixTime, formatDistanceStrict } from 'date-fns'; -import useMedia from 'use-media'; - -type RepoProps = Omit & { - repoManageEditHandler: () => void; - wizardEnv: Optional; -}; - -export default function Repo(props: RepoProps) { - const isMobile = useMedia({ maxWidth: 1000 }); - - const currentDate = useMemo(() => new Date(), []); - - //Load displayDetails from LocalStorage - const displayDetailsFromLS = (): boolean => { - const key = `displayDetailsRepo${props.id}`; - - try { - const storedValue = localStorage.getItem(key); - - if (storedValue === null) { - const defaultValue = true; - localStorage.setItem(key, JSON.stringify(defaultValue)); - return defaultValue; - } - - const parsedValue = JSON.parse(storedValue); - if (typeof parsedValue === 'boolean') { - return parsedValue; - } - - localStorage.removeItem(key); - return true; - } catch (error) { - localStorage.removeItem(key); - return true; - } - }; - - //States - const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS); - - //BUTTON : Display or not repo details for ONE repo - const displayDetailsForOneHandler = (boolean: boolean) => { - //Update localStorage - localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(boolean)); - setDisplayDetails(boolean); - }; - - //Status indicator - const statusIndicator = () => { - return props.status ? classes.statusIndicatorGreen : classes.statusIndicatorRed; - }; - - //Alert indicator - const alertIndicator = () => { - if (props.alert === 0) { - return ( -
- -
- ); - } - }; - - const appendOnlyModeIndicator = () => { - if (props.appendOnlyMode) { - return ( -
- -
- ); - } - }; - - const mobileView = () => { - return ( - <> -
-
-
-
-
{props.alias}
-
- {appendOnlyModeIndicator()} - {alertIndicator()} - {props.comment && ( -
- -
{props.comment}
-
- )} - -
- - {props.lastSave === 0 - ? '-' - : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { - addSuffix: true, - })} - -
-
-
- - ); - }; - - if (isMobile) { - return mobileView(); - } else { - return ( - <> - {displayDetails ? ( - <> -
-
-
- {props.comment && ( -
- -
{props.comment}
-
- )} - {appendOnlyModeIndicator()} - {alertIndicator()} - -
-
-
{props.alias}
-
- - - - - - - - - - - - - - - - - - - - -
RepositoryStorage SizeStorage UsedLast changeEdit
{props.repositoryName}{props.storageSize} GB - - -
- {props.lastSave === 0 - ? '-' - : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { - addSuffix: true, - })} -
-
-
- props.repoManageEditHandler()} - /> -
-
-
- - ) : ( - <> -
-
-
-
-
{props.alias}
-
- {appendOnlyModeIndicator()} - {alertIndicator()} - {props.comment && ( -
- -
{props.comment}
-
- )} - -
- - {props.lastSave === 0 - ? '-' - : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { - addSuffix: true, - })} - -
-
-
- - )} - {displayDetails ? ( -
- { - displayDetailsForOneHandler(false); - }} - /> -
- ) : ( -
- { - displayDetailsForOneHandler(true); - }} - /> -
- )} - - ); - } -} diff --git a/Components/UI/CopyButton/CopyButton.tsx b/Components/UI/CopyButton/CopyButton.js similarity index 77% rename from Components/UI/CopyButton/CopyButton.tsx rename to Components/UI/CopyButton/CopyButton.js index 1258f39..6de1f00 100644 --- a/Components/UI/CopyButton/CopyButton.tsx +++ b/Components/UI/CopyButton/CopyButton.js @@ -1,19 +1,14 @@ +//Lib import classes from './CopyButton.module.css'; -import { useState, ReactNode } from 'react'; +import { useState } from 'react'; import { IconChecks, IconCopy } from '@tabler/icons-react'; -type CopyButtonProps = { - dataToCopy: string; - children?: ReactNode; - displayIconConfirmation?: boolean; - size?: number; - stroke?: number; -}; - -export default function CopyButton(props: CopyButtonProps) { +export default function CopyButton(props) { + //State const [isCopied, setIsCopied] = useState(false); - const handleCopy = async (data: string) => { + //Function + const handleCopy = async (data) => { navigator.clipboard .writeText(data) .then(() => { diff --git a/Components/UI/Error/Error.tsx b/Components/UI/Error/Error.js similarity index 55% rename from Components/UI/Error/Error.tsx rename to Components/UI/Error/Error.js index d0c4da8..f677994 100644 --- a/Components/UI/Error/Error.tsx +++ b/Components/UI/Error/Error.js @@ -1,9 +1,6 @@ +//Lib import classes from './Error.module.css'; -type ErrorProps = { - message: string; -}; - -export default function Error(props: ErrorProps) { +export default function Error(props) { return
{props.message}
; } diff --git a/Components/UI/Info/Info.tsx b/Components/UI/Info/Info.js similarity index 54% rename from Components/UI/Info/Info.tsx rename to Components/UI/Info/Info.js index 8e2f729..5597ef9 100644 --- a/Components/UI/Info/Info.tsx +++ b/Components/UI/Info/Info.js @@ -1,13 +1,7 @@ -import { ReactNode } from 'react'; +//Lib import classes from './Info.module.css'; -type InfoProps = { - message: string; - color?: string; - children?: ReactNode; -}; - -export default function Info(props: InfoProps) { +export default function Info(props) { return (
{props.message} diff --git a/Components/UI/Layout/Footer/Footer.tsx b/Components/UI/Layout/Footer/Footer.js similarity index 87% rename from Components/UI/Layout/Footer/Footer.tsx rename to Components/UI/Layout/Footer/Footer.js index 58cd034..0e16c54 100644 --- a/Components/UI/Layout/Footer/Footer.tsx +++ b/Components/UI/Layout/Footer/Footer.js @@ -1,5 +1,6 @@ +//Lib import classes from './Footer.module.css'; -import packageInfo from '~/package.json'; +import packageInfo from '../../../../package.json'; function Footer() { return ( diff --git a/Components/UI/Layout/Header/Header.tsx b/Components/UI/Layout/Header/Header.js similarity index 50% rename from Components/UI/Layout/Header/Header.tsx rename to Components/UI/Layout/Header/Header.js index 1067f6e..d96a71d 100644 --- a/Components/UI/Layout/Header/Header.tsx +++ b/Components/UI/Layout/Header/Header.js @@ -1,21 +1,14 @@ -import Image from 'next/image'; +//Lib import classes from './Header.module.css'; + +//Components import Nav from './Nav/Nav'; function Header() { return (
-
- BorgWarehouse -
+
BorgWarehouse
diff --git a/Components/WizardSteps/WizardStep3/WizardStep3.tsx b/Components/WizardSteps/WizardStep3/WizardStep3.js similarity index 84% rename from Components/WizardSteps/WizardStep3/WizardStep3.tsx rename to Components/WizardSteps/WizardStep3/WizardStep3.js index 1b065f1..6c8706f 100644 --- a/Components/WizardSteps/WizardStep3/WizardStep3.tsx +++ b/Components/WizardSteps/WizardStep3/WizardStep3.js @@ -1,15 +1,16 @@ +//Lib import React from 'react'; import classes from '../WizardStep1/WizardStep1.module.css'; import { IconChecks, IconPlayerPlay } from '@tabler/icons-react'; import CopyButton from '../../UI/CopyButton/CopyButton'; -import { WizardStepProps } from '~/types'; -import { lanCommandOption } from '~/helpers/functions'; +import lanCommandOption from '../../../helpers/functions/lanCommandOption'; -function WizardStep3(props: WizardStepProps) { +function WizardStep3(props) { + ////Vars const wizardEnv = props.wizardEnv; - const UNIX_USER = wizardEnv?.UNIX_USER; + const UNIX_USER = wizardEnv.UNIX_USER; //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled. - const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand); + const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedOption.lanCommand); return (
@@ -30,11 +31,11 @@ function WizardStep3(props: WizardStepProps) { borg create ssh:// {UNIX_USER}@{FQDN} {SSH_SERVER_PORT}/./ - {props.selectedRepo?.repositoryName} + {props.selectedOption.repositoryName} ::archive1 /your/pathToBackup
@@ -69,10 +70,10 @@ function WizardStep3(props: WizardStepProps) { borg check -v --progress ssh:// {UNIX_USER}@{FQDN} {SSH_SERVER_PORT}/./ - {props.selectedRepo?.repositoryName} + {props.selectedOption.repositoryName}
  • List the remote archives with :
  • @@ -87,10 +88,10 @@ function WizardStep3(props: WizardStepProps) { borg list ssh:// {UNIX_USER}@{FQDN} {SSH_SERVER_PORT}/./ - {props.selectedRepo?.repositoryName} + {props.selectedOption.repositoryName}
  • Download a remote archive with the following command :
  • @@ -102,14 +103,14 @@ function WizardStep3(props: WizardStepProps) { }} >
    - borg export-tar --tar-filter="gzip -9" ssh:// + borg export-tar --tar-filter="gzip -9" ssh:// {UNIX_USER}@{FQDN} {SSH_SERVER_PORT}/./ - {props.selectedRepo?.repositoryName} + {props.selectedOption.repositoryName} ::archive1 archive1.tar.gz
  • Mount an archive to compare or backup some files without download all the archive :
  • @@ -124,11 +125,11 @@ function WizardStep3(props: WizardStepProps) { borg mount ssh:// {UNIX_USER}@{FQDN} {SSH_SERVER_PORT}/./ - {props.selectedRepo?.repositoryName} + {props.selectedOption.repositoryName} ::archive1 /tmp/yourMountPoint
    diff --git a/Components/WizardSteps/WizardStep4/WizardStep4.tsx b/Components/WizardSteps/WizardStep4/WizardStep4.js similarity index 81% rename from Components/WizardSteps/WizardStep4/WizardStep4.tsx rename to Components/WizardSteps/WizardStep4/WizardStep4.js index 17d355a..e989798 100644 --- a/Components/WizardSteps/WizardStep4/WizardStep4.tsx +++ b/Components/WizardSteps/WizardStep4/WizardStep4.js @@ -1,17 +1,18 @@ +//Lib import React from 'react'; import classes from '../WizardStep1/WizardStep1.module.css'; import { IconWand } from '@tabler/icons-react'; import CopyButton from '../../UI/CopyButton/CopyButton'; -import { WizardStepProps } from '~/types'; -import { lanCommandOption } from '~/helpers/functions'; +import lanCommandOption from '../../../helpers/functions/lanCommandOption'; -function WizardStep4(props: WizardStepProps) { +function WizardStep4(props) { + ////Vars const wizardEnv = props.wizardEnv; - const UNIX_USER = wizardEnv?.UNIX_USER; + const UNIX_USER = wizardEnv.UNIX_USER; //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled. - const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand); + const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedOption.lanCommand); - const configBorgmatic = ` + const configBorgmatic = `location: # List of source directories to backup. source_directories: - /your-repo-to-backup @@ -19,21 +20,24 @@ function WizardStep4(props: WizardStepProps) { repositories: # Paths of local or remote repositories to backup to. - - path: ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName} + - ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName} -archive_name_format: '{FQDN}-documents-{now}' -encryption_passphrase: "YOUR PASSPHRASE" +storage: + archive_name_format: '{FQDN}-documents-{now}' + encryption_passphrase: "YOUR PASSPHRASE" -# Retention policy for how many backups to keep. -keep_daily: 7 -keep_weekly: 4 -keep_monthly: 6 +retention: + # Retention policy for how many backups to keep. + keep_daily: 7 + keep_weekly: 4 + keep_monthly: 6 -# List of checks to run to validate your backups. -checks: - - name: repository - - name: archives - frequency: 2 weeks +consistency: + # List of checks to run to validate your backups. + checks: + - name: repository + - name: archives + frequency: 2 weeks #hooks: # Custom preparation scripts to run. diff --git a/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx b/Components/WizardSteps/WizardStepBar/WizardStepBar.js similarity index 90% rename from Components/WizardSteps/WizardStepBar/WizardStepBar.tsx rename to Components/WizardSteps/WizardStepBar/WizardStepBar.js index a6adb7c..757cbe5 100644 --- a/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx +++ b/Components/WizardSteps/WizardStepBar/WizardStepBar.js @@ -1,17 +1,12 @@ +//Lib import React from 'react'; import classes from './WizardStepBar.module.css'; import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; -type WizardStepBarProps = { - step: number; - setStep: (step: number) => void; - previousStepHandler: () => void; - nextStepHandler: () => void; -}; - -function WizardStepBar(props: WizardStepBarProps) { +function WizardStepBar(props) { + ////Functions //Color onClick on a step - const colorHandler = (step: number) => { + const colorHandler = (step) => { if (step <= props.step) { return classes.active; } else { diff --git a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js similarity index 79% rename from Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx rename to Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js index 3a9bd86..2443865 100644 --- a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx +++ b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js @@ -1,3 +1,4 @@ +//Lib import { Chart as ChartJS, CategoryScale, @@ -9,15 +10,16 @@ import { } from 'chart.js'; import { Bar } from 'react-chartjs-2'; import { useState, useEffect } from 'react'; -import { Repository, Optional } from '~/types'; export default function StorageUsedChartBar() { - const [data, setData] = useState>>(); + //States + const [data, setData] = useState([]); + //LifeCycle useEffect(() => { const dataFetch = async () => { try { - const response = await fetch('/api/v1/repositories', { + const response = await fetch('/api/repo', { method: 'GET', headers: { 'Content-type': 'application/json', @@ -39,10 +41,10 @@ export default function StorageUsedChartBar() { responsive: true, plugins: { legend: { - position: 'bottom' as const, + position: 'bottom', }, title: { - position: 'bottom' as const, + position: 'bottom', display: true, text: 'Storage used for each repository', }, @@ -53,7 +55,7 @@ export default function StorageUsedChartBar() { min: 0, ticks: { // Include a dollar sign in the ticks - callback: function (value: number | string) { + callback: function (value) { return value + '%'; }, stepSize: 10, @@ -62,7 +64,7 @@ export default function StorageUsedChartBar() { }, }; - const labels = data?.map((repo) => repo.alias); + const labels = data.map((repo) => repo.alias); const dataChart = { labels, @@ -70,7 +72,7 @@ export default function StorageUsedChartBar() { { label: 'Storage used (%)', //storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %. - data: data?.map((repo) => + data: data.map((repo) => (((repo.storageUsed / 1024 ** 2) * 100) / repo.storageSize).toFixed(1) ), backgroundColor: '#704dff', diff --git a/Containers/RepoList/RepoList.js b/Containers/RepoList/RepoList.js new file mode 100644 index 0000000..276eda6 --- /dev/null +++ b/Containers/RepoList/RepoList.js @@ -0,0 +1,159 @@ +//Lib +import classes from './RepoList.module.css'; +import React, { useState, useEffect } from 'react'; +import { IconPlus } from '@tabler/icons-react'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import useSWR, { useSWRConfig } from 'swr'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +//Composants +import Repo from '../../Components/Repo/Repo'; +import RepoManage from '../RepoManage/RepoManage'; +import ShimmerRepoList from '../../Components/UI/ShimmerRepoList/ShimmerRepoList'; + +export default function RepoList() { + ////Var + const router = useRouter(); + const { mutate } = useSWRConfig(); + const toastOptions = { + position: 'top-right', + autoClose: 8000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }; + + ////Datas + //Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format + const fetcher = async (url) => await fetch(url).then((res) => res.json()); + const { data, error } = useSWR('/api/repo', fetcher); + + ////LifeCycle + //Component did mount + useEffect(() => { + //If the route is home/manage-repo/add, open the RepoAdd box. + if (router.pathname === '/manage-repo/add') { + setDisplayRepoAdd(!displayRepoAdd); + } + //If the route is home/manage-repo/edit, open the RepoAdd box. + if (router.pathname.startsWith('/manage-repo/edit')) { + setDisplayRepoEdit(!displayRepoEdit); + } + //Fetch wizardEnv to hydrate Repo components + const fetchWizardEnv = async () => { + try { + const response = await fetch('/api/account/getWizardEnv', { + method: 'GET', + headers: { + 'Content-type': 'application/json', + }, + }); + setWizardEnv((await response.json()).wizardEnv); + } catch (error) { + console.log('Fetching datas error'); + } + }; + fetchWizardEnv(); + }, []); + + ////States + const [displayRepoAdd, setDisplayRepoAdd] = useState(false); + const [displayRepoEdit, setDisplayRepoEdit] = useState(false); + const [wizardEnv, setWizardEnv] = useState({}); + + ////Functions + + //Firstly, check the availability of data and condition it. + if (!data) { + //Force mutate after login (force a API GET on /api/repo to load repoList) + mutate('/api/repo'); + return ; + } + if (error) { + toast.error('An error has occurred.', toastOptions); + return ; + } + if (data.status == 500) { + toast.error('API Error !', toastOptions); + return ; + } + + //BUTTON : Display RepoManage component box for ADD + const manageRepoAddHandler = () => { + router.replace('/manage-repo/add'); + }; + + //BUTTON : Display RepoManage component box for EDIT + const repoManageEditHandler = (id) => { + router.replace('/manage-repo/edit/' + id); + }; + + //BUTTON : Close RepoManage component box (when cross is clicked) + const closeRepoManageBoxHandler = () => { + router.replace('/'); + }; + + // UI EFFECT : Display blur when display add repo modale + const displayBlur = () => { + if (displayRepoAdd || displayRepoEdit) { + return classes.containerBlur; + } else { + return classes.container; + } + }; + + //Dynamic list of repositories (with a map of Repo components) + const renderRepoList = data.repoList.map((repo, index) => { + return ( + + repoManageEditHandler(repo.id)} + wizardEnv={wizardEnv} + > + + ); + }); + + return ( + <> +
    +
    + + + Add a repository + +
    +
    +
    {renderRepoList}
    +
    +
    + {displayRepoAdd ? ( + + ) : null} + {displayRepoEdit ? ( + + ) : null} + + ); +} diff --git a/Containers/RepoList/RepoList.module.css b/Containers/RepoList/RepoList.module.css index 087d90b..11ff66b 100644 --- a/Containers/RepoList/RepoList.module.css +++ b/Containers/RepoList/RepoList.module.css @@ -86,6 +86,7 @@ flex-direction: column; width: 90%; margin: 5px auto; + padding: 15px; } .unfoldButton { @@ -122,77 +123,3 @@ display: none; } } - -/* Toolbar */ - -.toolbar { - display: flex; - justify-content: space-between; - align-items: center; - width: 90%; - margin: 20px auto 10px; - flex-wrap: wrap; - gap: 10px; -} - -.searchInput { - padding: 10px 15px; - border: 1px solid #ccc; - border-radius: 8px; - font-size: 14px; - width: 100%; - max-width: 300px; -} - -.sortIcons { - display: flex; - gap: 10px; - align-items: center; -} - -.icon { - cursor: pointer; - color: #a6a6b8; - transition: transform 0.2s ease; -} - -.icon:hover { - transform: scale(1.1); - color: #6d4aff; -} - -.iconActive { - color: #6d4aff; - transform: scale(1.2); -} - -.searchContainer { - position: relative; - display: flex; - align-items: center; - width: 100%; - max-width: 300px; -} - -.searchInput { - width: 100%; - padding: 8px 32px 8px 12px; - border-radius: 8px; - border: 1px solid #ccc; - font-size: 14px; - outline: none; - background-color: white; -} - -.clearButton { - position: absolute; - right: 8px; - background: transparent; - border: none; - cursor: pointer; - color: #999; -} - -.clearButton:hover { - color: #333; -} diff --git a/Containers/RepoList/RepoList.tsx b/Containers/RepoList/RepoList.tsx deleted file mode 100644 index d9160f0..0000000 --- a/Containers/RepoList/RepoList.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import classes from './RepoList.module.css'; -import React, { useState, useEffect } from 'react'; -import { - IconPlus, - IconSortAscendingLetters, - IconSortDescendingLetters, - IconSortAscending2, - IconSortDescending2, - IconDatabase, - IconX, - IconClock, - IconCalendarUp, - IconCalendarDown, - IconSortAscendingSmallBig, - IconSortDescendingSmallBig, - IconSortDescending2Filled, -} from '@tabler/icons-react'; -import { useRouter } from 'next/router'; -import Link from 'next/link'; -import useSWR, { useSWRConfig } from 'swr'; -import { ToastContainer, ToastOptions, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -import Repo from '~/Components/Repo/Repo'; -import RepoManage from '../RepoManage/RepoManage'; -import ShimmerRepoList from '~/Components/UI/ShimmerRepoList/ShimmerRepoList'; -import { Repository, WizardEnvType, Optional } from '~/types'; - -type SortOption = - | 'alias-asc' - | 'alias-desc' - | 'status-true' - | 'status-false' - | 'storage-used-asc' - | 'storage-used-desc' - | 'last-save-asc' - | 'last-save-desc'; - -export default function RepoList() { - const router = useRouter(); - const { mutate } = useSWRConfig(); - const [displayRepoAdd, setDisplayRepoAdd] = useState(false); - const [displayRepoEdit, setDisplayRepoEdit] = useState(false); - const [wizardEnv, setWizardEnv] = useState>(); - - const [sortOption, setSortOption] = useState(() => { - const savedSort = localStorage.getItem('repoSort'); - return (savedSort as SortOption) || 'alias-asc'; - }); - - const [searchQuery, setSearchQuery] = useState(() => { - const savedSearch = localStorage.getItem('repoSearch'); - return savedSearch || ''; - }); - - const toastOptions: ToastOptions = { - position: 'top-right', - autoClose: 8000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }; - - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - setDisplayRepoAdd(router.pathname === '/manage-repo/add'); - setDisplayRepoEdit(router.pathname.startsWith('/manage-repo/edit')); - - const fetchWizardEnv = async () => { - try { - const response = await fetch('/api/v1/account/wizard-env'); - const data: WizardEnvType = await response.json(); - setWizardEnv(data); - } catch (error) { - console.log('Fetching wizard env error'); - } - }; - fetchWizardEnv(); - }, [router.pathname]); - - const fetcher = async (url: string) => await fetch(url).then((res) => res.json()); - const { data, error } = useSWR('/api/v1/repositories', fetcher); - - if (!data) { - mutate('/api/v1/repositories'); - return ; - } - - if (error || data.status == 500) { - toast.error('Error loading repositories.', toastOptions); - return ; - } - - const handleSortChange = (option: SortOption) => { - setSortOption(option); - localStorage.setItem('repoSort', option); - }; - - const handleSearchChange = (e: React.ChangeEvent) => { - const query = e.target.value; - setSearchQuery(query); - localStorage.setItem('repoSearch', query); - }; - - const getSortedRepoList = () => { - let repoList = [...data.repoList]; - - // Filter - if (searchQuery) { - repoList = repoList.filter((repo) => - `${repo.alias} ${repo.comment} ${repo.repositoryName}` - .toLowerCase() - .includes(searchQuery.toLowerCase()) - ); - } - - // Sort - switch (sortOption) { - case 'alias-asc': - return repoList.sort((a, b) => a.alias.localeCompare(b.alias)); - case 'alias-desc': - return repoList.sort((a, b) => b.alias.localeCompare(a.alias)); - case 'status-true': - return repoList.sort((a, b) => Number(b.status) - Number(a.status)); - case 'status-false': - return repoList.sort((a, b) => Number(a.status) - Number(b.status)); - case 'storage-used-asc': - return repoList.sort((a, b) => { - const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0; - const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0; - return aRatio - bRatio; - }); - case 'storage-used-desc': - return repoList.sort((a, b) => { - const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0; - const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0; - return bRatio - aRatio; - }); - case 'last-save-asc': - return repoList.sort((a, b) => { - const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0; - const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0; - return aDate - bDate; - }); - case 'last-save-desc': - return repoList.sort((a, b) => { - const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0; - const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0; - return bDate - aDate; - }); - default: - return repoList; - } - }; - - const manageRepoAddHandler = () => router.replace('/manage-repo/add'); - const manageRepoEditHandler = (id: number) => router.replace('/manage-repo/edit/' + id); - const closeRepoManageBoxHandler = () => router.replace('/'); - const displayBlur = () => - displayRepoAdd || displayRepoEdit ? classes.containerBlur : classes.container; - - const renderRepoList = getSortedRepoList().map((repo: Repository) => ( - manageRepoEditHandler(repo.id)} - wizardEnv={wizardEnv} - /> - )); - - return ( - <> -
    -
    - - - Add a repository - -
    - -
    -
    - - {searchQuery && ( - - )} -
    - -
    - handleSortChange('alias-asc')} - title='Alias A-Z' - /> - handleSortChange('alias-desc')} - title='Alias Z-A' - /> - handleSortChange('status-true')} - title='Status OK β†’ KO' - /> - handleSortChange('status-false')} - title='Status KO β†’ OK' - /> - handleSortChange('last-save-desc')} - title='Last save (recent β†’ old)' - /> - handleSortChange('last-save-asc')} - title='Last save (old β†’ recent)' - /> - handleSortChange('storage-used-asc')} - title='Storage usage % low β†’ high' - /> - handleSortChange('storage-used-desc')} - title='Storage usage % high β†’ low' - /> -
    -
    - -
    -
    {renderRepoList}
    -
    -
    - - {displayRepoAdd && ( - - )} - {displayRepoEdit && ( - - )} - - ); -} diff --git a/Containers/RepoManage/RepoManage.tsx b/Containers/RepoManage/RepoManage.js similarity index 52% rename from Containers/RepoManage/RepoManage.tsx rename to Containers/RepoManage/RepoManage.js index b37f0cd..a758efa 100644 --- a/Containers/RepoManage/RepoManage.tsx +++ b/Containers/RepoManage/RepoManage.js @@ -1,46 +1,29 @@ -import { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import Select from 'react-select'; -import { toast, ToastOptions } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; -import { useLoader } from '~/contexts/LoaderContext'; -import { alertOptions, Optional, Repository } from '~/types'; +//Lib import classes from './RepoManage.module.css'; +import { IconAlertCircle, IconX } from '@tabler/icons-react'; +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import { useForm, Controller } from 'react-hook-form'; +import { SpinnerDotted } from 'spinners-react'; +import Select from 'react-select'; +import Link from 'next/link'; +import { IconExternalLink } from '@tabler/icons-react'; +import { alertOptions } from '../../domain/constants'; -type RepoManageProps = { - mode: 'add' | 'edit'; - repoList: Optional>; - closeHandler: () => void; -}; - -type DataForm = { - alias: string; - storageSize: string; - sshkey: string; - comment: string; - alert: { value: Optional; label: string }; - lanCommand: boolean; - appendOnlyMode: boolean; -}; - -export default function RepoManage(props: RepoManageProps) { +export default function RepoManage(props) { + ////Var + let targetRepo; const router = useRouter(); - const targetRepo = - props.mode === 'edit' && router.query.slug - ? props.repoList?.find((repo) => repo.id.toString() === router.query.slug) - : undefined; - const { register, handleSubmit, control, formState: { errors, isSubmitting, isValid }, - } = useForm({ mode: 'onChange' }); + } = useForm({ mode: 'onChange' }); - const toastOptions: ToastOptions = { + const toastOptions = { position: 'top-right', autoClose: 5000, hideProgressBar: false, @@ -50,113 +33,108 @@ export default function RepoManage(props: RepoManageProps) { progress: undefined, }; + ////State const [deleteDialog, setDeleteDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { start, stop } = useLoader(); + ////Functions //router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484). //If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering. - if (props.mode === 'edit') { - if (!router.query.slug) { - start(); - return; - } else if (!targetRepo) { - stop(); + if (!router.query.slug && props.mode == 'edit') { + return ; + } else if (props.mode == 'edit') { + for (let element in props.repoList) { + if (props.repoList[element].id == router.query.slug) { + targetRepo = props.repoList[element]; + } + } + //If the ID does not exist > 404 + if (!targetRepo) { router.push('/404'); + return null; } } //Delete a repo - const deleteHandler = async (repositoryName?: string) => { - start(); - if (!repositoryName) { - stop(); - toast.error('Repository name not found', toastOptions); - router.replace('/'); - return; - } + const deleteHandler = async () => { //API Call for delete - await fetch('/api/v1/repositories/' + repositoryName, { + fetch('/api/repo/id/' + router.query.slug + '/delete', { method: 'DELETE', headers: { 'Content-type': 'application/json', }, }) - .then(async (response) => { + .then((response) => { if (response.ok) { toast.success( - 'πŸ—‘ The repository ' + repositoryName + ' has been successfully deleted', + 'πŸ—‘ The repository #' + router.query.slug + ' has been successfully deleted', toastOptions ); router.replace('/'); } else { - if (response.status == 403) { + if (response.status == 403) toast.warning( 'πŸ”’ The server is currently protected against repository deletion.', toastOptions ); - setIsLoading(false); - router.replace('/'); - } else { - const errorMessage = await response.json(); - toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); - router.replace('/'); - console.log('Fail to delete'); - } + else toast.error('An error has occurred', toastOptions); + router.replace('/'); + console.log('Fail to delete'); } }) .catch((error) => { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); - }) - .finally(() => { - stop(); }); }; - const isSSHKeyUnique = async (sshPublicKey: string): Promise => { - try { - // Extract the first two columns of the SSH key in the form - const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' '); + //Verify that the SSH key is unique + const isSSHKeyUnique = async (sshPublicKey) => { + let isUnique = true; - const response = await fetch('/api/v1/repositories', { method: 'GET' }); - const data: { repoList: Repository[] } = await response.json(); + // Extract the first two columns of the SSH key in the form + const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' '); - const conflictingRepo = data.repoList.find((repo: { sshPublicKey: string; id: number }) => { - const repoPublicKeyPrefix = repo.sshPublicKey.split(' ').slice(0, 2).join(' '); - return ( - repoPublicKeyPrefix === publicKeyPrefix && (!targetRepo || repo.id !== targetRepo.id) - ); + await fetch('/api/repo', { method: 'GET' }) + .then((response) => response.json()) + .then((data) => { + for (let element in data.repoList) { + // Extract the first two columns of the SSH key in the repoList + const repoPublicKeyPrefix = data.repoList[element].sshPublicKey + .split(' ') + .slice(0, 2) + .join(' '); + + if ( + repoPublicKeyPrefix === publicKeyPrefix && // Compare the first two columns of the SSH key + (!targetRepo || data.repoList[element].id != targetRepo.id) + ) { + toast.error( + 'The SSH key is already used in repository #' + + data.repoList[element].id + + '. Please use another key or delete the key from the other repository.', + toastOptions + ); + isUnique = false; + break; + } + } + }) + .catch((error) => { + console.log(error); + toast.error('An error has occurred', toastOptions); + isUnique = false; }); - - if (conflictingRepo) { - toast.error( - `The SSH key is already used in repository ${conflictingRepo.repositoryName}. Please use another key or delete the key from the other repository.`, - toastOptions - ); - return false; - } - - return true; - } catch (error) { - console.log(error); - toast.error('An error has occurred', toastOptions); - return false; - } + return isUnique; }; //Form submit Handler for ADD or EDIT a repo - const formSubmitHandler = async (dataForm: DataForm) => { + const formSubmitHandler = async (dataForm) => { + //Loading button on submit to avoid multiple send. setIsLoading(true); - start(); - - // Clean SSH key by removing leading/trailing whitespace and line breaks - const cleanedSSHKey = dataForm.sshkey.trim(); - //Verify that the SSH key is unique - if (!(await isSSHKeyUnique(cleanedSSHKey))) { - stop(); + if (!(await isSSHKeyUnique(dataForm.sshkey))) { setIsLoading(false); return; } @@ -165,14 +143,14 @@ export default function RepoManage(props: RepoManageProps) { const newRepo = { alias: dataForm.alias, storageSize: parseInt(dataForm.storageSize), - sshPublicKey: cleanedSSHKey, + sshPublicKey: dataForm.sshkey, comment: dataForm.comment, alert: dataForm.alert.value, lanCommand: dataForm.lanCommand, appendOnlyMode: dataForm.appendOnlyMode, }; //POST API to send new repo - await fetch('/api/v1/repositories', { + await fetch('/api/repo/add', { method: 'POST', headers: { 'Content-type': 'application/json', @@ -185,7 +163,7 @@ export default function RepoManage(props: RepoManageProps) { router.replace('/'); } else { const errorMessage = await response.json(); - toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); + toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions); router.replace('/'); console.log(`Fail to ${props.mode}`); } @@ -194,23 +172,19 @@ export default function RepoManage(props: RepoManageProps) { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); - }) - .finally(() => { - stop(); - setIsLoading(false); }); //EDIT a repo } else if (props.mode == 'edit') { const dataEdited = { alias: dataForm.alias, storageSize: parseInt(dataForm.storageSize), - sshPublicKey: cleanedSSHKey, + sshPublicKey: dataForm.sshkey, comment: dataForm.comment, alert: dataForm.alert.value, lanCommand: dataForm.lanCommand, appendOnlyMode: dataForm.appendOnlyMode, }; - await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, { + await fetch('/api/repo/id/' + router.query.slug + '/edit', { method: 'PATCH', headers: { 'Content-type': 'application/json', @@ -220,13 +194,13 @@ export default function RepoManage(props: RepoManageProps) { .then(async (response) => { if (response.ok) { toast.success( - 'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !', + 'The repository #' + targetRepo.id + ' has been successfully edited !', toastOptions ); router.replace('/'); } else { const errorMessage = await response.json(); - toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions); + toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions); router.replace('/'); console.log(`Fail to ${props.mode}`); } @@ -235,10 +209,6 @@ export default function RepoManage(props: RepoManageProps) { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); - }) - .finally(() => { - stop(); - setIsLoading(false); }); } }; @@ -261,54 +231,54 @@ export default function RepoManage(props: RepoManageProps) { color: 'rgba(99, 115, 129, 0.38)', }} > - {targetRepo?.repositoryName} + #{targetRepo.id} {' '} ?
    - You are about to permanently delete the repository{' '} - {targetRepo?.repositoryName} and all the backups it contains. + You are about to permanently delete the repository #{targetRepo.id} and all + the backups it contains.
    The data will not be recoverable and it will not be possible to go back.
    - <> - - - + {isLoading ? ( + + ) : ( + <> + + + + )}
    ) : (
    {props.mode == 'edit' && ( -

    +

    Edit the repository{' '} - {targetRepo?.repositoryName} + #{targetRepo.id} -

    + )} - {props.mode == 'add' &&

    Add a repository

    } + {props.mode == 'add' &&

    Add a repository

    }
    {/* ALIAS */} @@ -316,16 +286,16 @@ export default function RepoManage(props: RepoManageProps) { className='form-control is-invalid' placeholder='Alias for the repository, e.g."Server 1"' type='text' - defaultValue={props.mode == 'edit' ? targetRepo?.alias : undefined} + defaultValue={props.mode == 'edit' ? targetRepo.alias : null} {...register('alias', { required: 'An alias is required.', minLength: { - value: 1, - message: '1 character min', + value: 2, + message: '2 characters min', }, maxLength: { - value: 100, - message: '100 characters max', + value: 40, + message: '40 characters max', }, })} /> @@ -334,17 +304,15 @@ export default function RepoManage(props: RepoManageProps) {