diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs
index 2337e7e..e3610d9 100644
--- a/.commitlintrc.mjs
+++ b/.commitlintrc.mjs
@@ -1,4 +1,4 @@
-export default {
+const config = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
@@ -19,8 +19,12 @@ export default {
'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 68d3bf0..a2e1c7c 100644
--- a/.env.sample
+++ b/.env.sample
@@ -23,8 +23,6 @@ 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
new file mode 100644
index 0000000..136334e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+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
new file mode 100644
index 0000000..8aa78e6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/i-need-help.md
@@ -0,0 +1,21 @@
+---
+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 3110836..34a7437 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,16 +1,18 @@
version: 2
updates:
- - package-ecosystem: "docker"
- directory: "/"
+ - package-ecosystem: 'docker'
+ directory: '/'
schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/"
+ 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: '/'
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 cbc945a..825fd0d 100644
--- a/.github/workflows/bats.yml
+++ b/.github/workflows/bats.yml
@@ -1,5 +1,8 @@
name: Bats
+permissions:
+ contents: read
+
on:
push:
branches:
@@ -16,7 +19,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- 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 4ce762a..14c33a5 100644
--- a/.github/workflows/docker-image-develop.yml
+++ b/.github/workflows/docker-image-develop.yml
@@ -1,29 +1,38 @@
name: Build and Push Docker Image for Develop Branch
on:
- push:
- branches:
- - 'develop'
+ push:
+ branches:
+ - 'develop'
+
+permissions:
+ contents: read
jobs:
- docker:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Build and push
- uses: docker/build-push-action@v6
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
- tags: borgwarehouse/borgwarehouse:develop
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ - name: Update package.json version
+ run: |
+ COMMIT=$(git rev-parse --short HEAD)
+ echo "Current Commit: $COMMIT"
+ jq '.version = "develop-'$COMMIT'"' package.json > package.tmp.json
+ mv package.tmp.json package.json
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
+ tags: borgwarehouse/borgwarehouse:develop
diff --git a/.github/workflows/docker-image-latest.yml b/.github/workflows/docker-image-latest.yml
index f3a4554..7957cb8 100644
--- a/.github/workflows/docker-image-latest.yml
+++ b/.github/workflows/docker-image-latest.yml
@@ -1,4 +1,6 @@
name: Build and Push Docker Image
+permissions:
+ contents: read
on:
push:
@@ -10,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- 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 eabb0d2..01f4eb5 100644
--- a/.github/workflows/docker-image-release.yml
+++ b/.github/workflows/docker-image-release.yml
@@ -5,12 +5,15 @@ on:
types:
- published
+permissions:
+ contents: read
+
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- 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 55e4ad1..4d716ad 100644
--- a/.github/workflows/docker-image-test.yml
+++ b/.github/workflows/docker-image-test.yml
@@ -1,5 +1,8 @@
name: Test to build docker container on Pull Request
+permissions:
+ contents: read
+
on:
pull_request:
branches:
@@ -11,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- 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 92522ae..8b954af 100644
--- a/.github/workflows/shellcheck.yml
+++ b/.github/workflows/shellcheck.yml
@@ -4,19 +4,21 @@ on:
- main
- develop
pull_request:
- branches: main
+ branches:
+ - main
+ - develop
-name: "Shellcheck"
+name: 'Shellcheck'
permissions: {}
jobs:
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
-
+
steps:
- - uses: actions/checkout@v4
-
+ - uses: actions/checkout@v6
+
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml
new file mode 100644
index 0000000..fe9d512
--- /dev/null
+++ b/.github/workflows/vitest.yml
@@ -0,0 +1,63 @@
+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 53f9ab5..c95738d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,14 @@ 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 8990b55..a54594a 100755
--- a/.husky/append-icon.sh
+++ b/.husky/append-icon.sh
@@ -23,41 +23,43 @@ function checkBreakingChangeInBody() {
}
function findTypeIcon() {
- # get message from 1st param
message="$1"
- # 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]='π§'
+ if [[ "$message" =~ ^.*!:\ .* ]]; then
+ echo "$boomIcon"
+ return 0
+ 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
+ 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
}
# extract original message from the first line of file
diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg
index 5942463..d48435c 100755
--- a/.husky/prepare-commit-msg
+++ b/.husky/prepare-commit-msg
@@ -1,6 +1,3 @@
-#!/bin/bash
-. "$(dirname "$0")/_/husky.sh"
-
# Check if it's an amend commit
if [ "$2" = "commit" ]; then
echo "Amendment detected, appending icon..."
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..cbcc181
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,7 @@
+# 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.module.css b/Components/Repo/QuickCommands/QuickCommands.module.css
index faba43d..77cc329 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 47px auto auto;
+ margin: auto 25px auto auto;
}
.icons {
@@ -31,7 +31,7 @@
width: 100%;
height: 100%;
border: 1px solid #6d4aff21;
- background-color: #f5f5f5;
+ background-color: #fafafa;
border-radius: 5px;
box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
color: #65748b;
@@ -50,6 +50,7 @@
.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;
@@ -76,7 +77,7 @@
width: 100%;
height: 100%;
border: 1px solid #6d4aff21;
- background-color: #f5f5f5;
+ background-color: #fafafa;
border-radius: 5px;
box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
color: #65748b;
diff --git a/Components/Repo/QuickCommands/QuickCommands.js b/Components/Repo/QuickCommands/QuickCommands.tsx
similarity index 72%
rename from Components/Repo/QuickCommands/QuickCommands.js
rename to Components/Repo/QuickCommands/QuickCommands.tsx
index d32002e..8fe57b3 100644
--- a/Components/Repo/QuickCommands/QuickCommands.js
+++ b/Components/Repo/QuickCommands/QuickCommands.tsx
@@ -1,26 +1,30 @@
-//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/lanCommandOption';
+import { lanCommandOption } from '~/helpers/functions';
+import { WizardEnvType } from '~/types/domain/config.types';
-export default function QuickCommands(props) {
- ////Vars
+type QuickCommandsProps = {
+ repositoryName: string;
+ wizardEnv?: WizardEnvType;
+ lanCommand?: boolean;
+};
+
+export default function QuickCommands(props: QuickCommandsProps) {
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}/./${props.repositoryName}`)
+ .writeText(
+ `ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./${props.repositoryName}`
+ )
.then(() => {
- // If successful, update the isCopied state value
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
@@ -37,8 +41,8 @@ export default function QuickCommands(props) {
Copied !
) : (
- ssh://{wizardEnv.UNIX_USER}@{FQDN}
- {SSH_SERVER_PORT}/./
+ ssh://{wizardEnv?.UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./
{props.repositoryName}
)}
diff --git a/Components/Repo/Repo.js b/Components/Repo/Repo.js
deleted file mode 100644
index 1e2312a..0000000
--- a/Components/Repo/Repo.js
+++ /dev/null
@@ -1,180 +0,0 @@
-//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 && (
-
- )}
-
-
-
-
-
-
- Repository
- Storage Size
- Storage Used
- Last change
- ID
- Edit
-
-
-
-
- {props.repositoryName}
- {props.storageSize} GB
-
-
-
-
-
- {props.lastSave === 0 ? '-' : timestampConverter(props.lastSave)}
-
-
- #{props.id}
-
-
- props.repoManageEditHandler()}
- />
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
{props.alias}
- {appendOnlyModeIndicator()}
- {alertIndicator()}
- {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 d2f52fe..8311137 100644
--- a/Components/Repo/Repo.module.css
+++ b/Components/Repo/Repo.module.css
@@ -13,16 +13,39 @@
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 {
- padding: 15px;
+ 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;
}
/* REPO OPEN */
@@ -35,7 +58,6 @@
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;
@@ -43,15 +65,29 @@
overflow: visible;
/* Need to display comment on hover (which is position : absolute) */
position: relative;
+ background: #fff;
}
.openFlex {
- display: flex;
- align-items: center;
- align-self: flex-start;
+ display: block;
width: 100%;
}
+.aliasFlex {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+}
+.indicatorsFlex {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-end;
+ width: 100%;
+ gap: 15px;
+}
+
.tabInfo {
width: 100%;
overflow-wrap: break-word;
@@ -59,7 +95,7 @@
background: #fff;
border-radius: 10px;
overflow: hidden;
- margin: 25px auto;
+ margin: 15px auto;
table-layout: fixed;
}
@@ -73,7 +109,7 @@
font-size: 1em;
color: #fff;
line-height: 1.2;
- font-weight: normal;
+ font-weight: 500;
}
.tabInfo tbody tr {
@@ -88,80 +124,52 @@
}
/*STATUS*/
-
-.statusIndicatorGreen {
- background: rgb(9, 255, 0);
+.statusIndicatorGreen,
+.statusIndicatorRed {
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;
+ height: 16px;
+ width: 16px;
+ flex-shrink: 0;
+ animation: pulse 5s infinite;
}
-@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);
- }
+.statusIndicatorGreen {
+ background: #00d26a;
+ box-shadow: 0 0 0 0 rgba(0, 210, 106, 0.7);
}
.statusIndicatorRed {
- 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;
+ background: #ff3d3d;
+ box-shadow: 0 0 0 0 rgba(255, 61, 61, 0.7);
animation-delay: 0.5s;
}
-@keyframes pulseRed {
+@keyframes pulse {
0% {
transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7);
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4);
}
-
10% {
transform: scale(1);
- box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
+ box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
-
90% {
transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(255, 0, 0, 0);
+ box-shadow: 0 0 0 0 rgba(0, 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 */
@@ -169,6 +177,13 @@
font-weight: bold;
color: #111827;
font-size: 1.05em;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.RepoOpen .alias {
+ margin-top: 5px;
}
.lastSave {
@@ -184,7 +199,6 @@
display: flex;
flex-direction: row;
align-items: center;
- margin-left: 10px;
}
.toolTip {
@@ -227,23 +241,69 @@
/* MOBILE */
@media all and (max-width: 1000px) {
- .tabInfo {
- display: none;
+ .openFlex,
+ .tabInfo,
+ .toolTip,
+ .comment,
+ .chevron {
+ display: none !important;
}
- .toolTip {
- display: none;
- }
- .comment {
- display: none;
- }
- .lastSave {
- 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;
}
+
.closeFlex {
- margin: auto;
+ display: flex !important;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0 !important;
+ margin: 0 !important;
}
- .openFlex {
- margin: auto;
- width: auto;
+
+ .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;
+ }
+
+ .appendOnlyModeIcon,
+ .alertIcon {
+ display: flex;
+ align-items: center;
}
}
diff --git a/Components/Repo/Repo.tsx b/Components/Repo/Repo.tsx
new file mode 100644
index 0000000..a659670
--- /dev/null
+++ b/Components/Repo/Repo.tsx
@@ -0,0 +1,261 @@
+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 (
+ <>
+
+
+
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+ {props.comment && (
+
+ )}
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ >
+ );
+ };
+
+ if (isMobile) {
+ return mobileView();
+ } else {
+ return (
+ <>
+ {displayDetails ? (
+ <>
+
+
+
+ {props.comment && (
+
+ )}
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+
+
+
+
+
+
+
+ Repository
+ Storage Size
+ Storage Used
+ Last change
+ Edit
+
+
+
+
+ {props.repositoryName}
+ {props.storageSize} GB
+
+
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ props.repoManageEditHandler()}
+ />
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+ {props.comment && (
+
+ )}
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ >
+ )}
+ {displayDetails ? (
+
+ {
+ displayDetailsForOneHandler(false);
+ }}
+ />
+
+ ) : (
+
+ {
+ displayDetailsForOneHandler(true);
+ }}
+ />
+
+ )}
+ >
+ );
+ }
+}
diff --git a/Components/UI/CopyButton/CopyButton.js b/Components/UI/CopyButton/CopyButton.tsx
similarity index 77%
rename from Components/UI/CopyButton/CopyButton.js
rename to Components/UI/CopyButton/CopyButton.tsx
index 6de1f00..1258f39 100644
--- a/Components/UI/CopyButton/CopyButton.js
+++ b/Components/UI/CopyButton/CopyButton.tsx
@@ -1,14 +1,19 @@
-//Lib
import classes from './CopyButton.module.css';
-import { useState } from 'react';
+import { useState, ReactNode } from 'react';
import { IconChecks, IconCopy } from '@tabler/icons-react';
-export default function CopyButton(props) {
- //State
+type CopyButtonProps = {
+ dataToCopy: string;
+ children?: ReactNode;
+ displayIconConfirmation?: boolean;
+ size?: number;
+ stroke?: number;
+};
+
+export default function CopyButton(props: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false);
- //Function
- const handleCopy = async (data) => {
+ const handleCopy = async (data: string) => {
navigator.clipboard
.writeText(data)
.then(() => {
diff --git a/Components/UI/Error/Error.js b/Components/UI/Error/Error.tsx
similarity index 55%
rename from Components/UI/Error/Error.js
rename to Components/UI/Error/Error.tsx
index f677994..d0c4da8 100644
--- a/Components/UI/Error/Error.js
+++ b/Components/UI/Error/Error.tsx
@@ -1,6 +1,9 @@
-//Lib
import classes from './Error.module.css';
-export default function Error(props) {
+type ErrorProps = {
+ message: string;
+};
+
+export default function Error(props: ErrorProps) {
return {props.message}
;
}
diff --git a/Components/UI/Info/Info.js b/Components/UI/Info/Info.tsx
similarity index 54%
rename from Components/UI/Info/Info.js
rename to Components/UI/Info/Info.tsx
index 5597ef9..8e2f729 100644
--- a/Components/UI/Info/Info.js
+++ b/Components/UI/Info/Info.tsx
@@ -1,7 +1,13 @@
-//Lib
+import { ReactNode } from 'react';
import classes from './Info.module.css';
-export default function Info(props) {
+type InfoProps = {
+ message: string;
+ color?: string;
+ children?: ReactNode;
+};
+
+export default function Info(props: InfoProps) {
return (
{props.message}
diff --git a/Components/UI/Layout/Footer/Footer.js b/Components/UI/Layout/Footer/Footer.tsx
similarity index 87%
rename from Components/UI/Layout/Footer/Footer.js
rename to Components/UI/Layout/Footer/Footer.tsx
index 0e16c54..58cd034 100644
--- a/Components/UI/Layout/Footer/Footer.js
+++ b/Components/UI/Layout/Footer/Footer.tsx
@@ -1,6 +1,5 @@
-//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.module.css b/Components/UI/Layout/Header/Header.module.css
index d90271d..2156d01 100644
--- a/Components/UI/Layout/Header/Header.module.css
+++ b/Components/UI/Layout/Header/Header.module.css
@@ -27,5 +27,5 @@
font-weight: bold;
color: #6d4aff;
text-shadow: #6d4aff 0px 0px 18px;
- margin-left: 20px;
+ margin-left: 70px;
}
diff --git a/Components/UI/Layout/Header/Header.js b/Components/UI/Layout/Header/Header.tsx
similarity index 50%
rename from Components/UI/Layout/Header/Header.js
rename to Components/UI/Layout/Header/Header.tsx
index d96a71d..1067f6e 100644
--- a/Components/UI/Layout/Header/Header.js
+++ b/Components/UI/Layout/Header/Header.tsx
@@ -1,14 +1,21 @@
-//Lib
+import Image from 'next/image';
import classes from './Header.module.css';
-
-//Components
import Nav from './Nav/Nav';
function Header() {
return (
-
);
}
diff --git a/Components/WizardSteps/WizardStep2/WizardStep2.js b/Components/WizardSteps/WizardStep2/WizardStep2.tsx
similarity index 78%
rename from Components/WizardSteps/WizardStep2/WizardStep2.js
rename to Components/WizardSteps/WizardStep2/WizardStep2.tsx
index d9b634a..e4f760b 100644
--- a/Components/WizardSteps/WizardStep2/WizardStep2.js
+++ b/Components/WizardSteps/WizardStep2/WizardStep2.tsx
@@ -1,16 +1,14 @@
-//Lib
-import React from 'react';
-import classes from '../WizardStep1/WizardStep1.module.css';
-import { IconTool, IconAlertCircle } from '@tabler/icons-react';
+import { IconAlertCircle, IconTool } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
-import lanCommandOption from '../../../helpers/functions/lanCommandOption';
+import { WizardStepProps } from '~/types';
+import classes from '../WizardStep1/WizardStep1.module.css';
+import { lanCommandOption } from '~/helpers/functions';
-function WizardStep2(props) {
- ////Vars
+function WizardStep2(props: WizardStepProps) {
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.selectedOption.lanCommand);
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
return (
@@ -32,10 +30,10 @@ function WizardStep2(props) {
borg init -e repokey-blake2 ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
@@ -71,8 +69,8 @@ function WizardStep2(props) {
Pika, Vorta...
- To "Initialize a new repository" or "Add existing repository", copy this into the field
- "Repository URL" of your graphical client :
+ To "Initialize a new repository" or "Add existing repository", copy this
+ into the field "Repository URL" of your graphical client :
@@ -100,19 +98,21 @@ function WizardStep2(props) {
Check the fingerprint of server
To check that you are talking to the right server, please make sure to validate one of the
- following key's fingerprint when you first connect :
+ following key's fingerprint when you first connect :
- ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
+ ECDSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_ECDSA}
- ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
+ ED25519 : {wizardEnv?.SSH_SERVER_FINGERPRINT_ED25519}
- RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}
+
+ RSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_RSA}
+
diff --git a/Components/WizardSteps/WizardStep3/WizardStep3.js b/Components/WizardSteps/WizardStep3/WizardStep3.tsx
similarity index 84%
rename from Components/WizardSteps/WizardStep3/WizardStep3.js
rename to Components/WizardSteps/WizardStep3/WizardStep3.tsx
index 6c8706f..1b065f1 100644
--- a/Components/WizardSteps/WizardStep3/WizardStep3.js
+++ b/Components/WizardSteps/WizardStep3/WizardStep3.tsx
@@ -1,16 +1,15 @@
-//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 lanCommandOption from '../../../helpers/functions/lanCommandOption';
+import { WizardStepProps } from '~/types';
+import { lanCommandOption } from '~/helpers/functions';
-function WizardStep3(props) {
- ////Vars
+function WizardStep3(props: WizardStepProps) {
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.selectedOption.lanCommand);
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
return (
@@ -31,11 +30,11 @@ function WizardStep3(props) {
borg create ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
::archive1 /your/pathToBackup
@@ -70,10 +69,10 @@ function WizardStep3(props) {
borg check -v --progress ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
List the remote archives with :
@@ -88,10 +87,10 @@ function WizardStep3(props) {
borg list ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
Download a remote archive with the following command :
@@ -103,14 +102,14 @@ function WizardStep3(props) {
}}
>
- borg export-tar --tar-filter="gzip -9" ssh://
+ borg export-tar --tar-filter="gzip -9" ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
::archive1 archive1.tar.gz
Mount an archive to compare or backup some files without download all the archive :
@@ -125,11 +124,11 @@ function WizardStep3(props) {
borg mount ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
+ {props.selectedRepo?.repositoryName}
::archive1 /tmp/yourMountPoint
diff --git a/Components/WizardSteps/WizardStep4/WizardStep4.js b/Components/WizardSteps/WizardStep4/WizardStep4.tsx
similarity index 81%
rename from Components/WizardSteps/WizardStep4/WizardStep4.js
rename to Components/WizardSteps/WizardStep4/WizardStep4.tsx
index e989798..17d355a 100644
--- a/Components/WizardSteps/WizardStep4/WizardStep4.js
+++ b/Components/WizardSteps/WizardStep4/WizardStep4.tsx
@@ -1,18 +1,17 @@
-//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 lanCommandOption from '../../../helpers/functions/lanCommandOption';
+import { WizardStepProps } from '~/types';
+import { lanCommandOption } from '~/helpers/functions';
-function WizardStep4(props) {
- ////Vars
+function WizardStep4(props: WizardStepProps) {
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.selectedOption.lanCommand);
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
- const configBorgmatic = `location:
+ const configBorgmatic = `
# List of source directories to backup.
source_directories:
- /your-repo-to-backup
@@ -20,24 +19,21 @@ function WizardStep4(props) {
repositories:
# Paths of local or remote repositories to backup to.
- - ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
+ - path: ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}
-storage:
- archive_name_format: '{FQDN}-documents-{now}'
- encryption_passphrase: "YOUR PASSPHRASE"
+archive_name_format: '{FQDN}-documents-{now}'
+encryption_passphrase: "YOUR PASSPHRASE"
-retention:
- # Retention policy for how many backups to keep.
- keep_daily: 7
- keep_weekly: 4
- keep_monthly: 6
+# Retention policy for how many backups to keep.
+keep_daily: 7
+keep_weekly: 4
+keep_monthly: 6
-consistency:
- # List of checks to run to validate your backups.
- checks:
- - name: repository
- - name: archives
- frequency: 2 weeks
+# 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.js b/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx
similarity index 90%
rename from Components/WizardSteps/WizardStepBar/WizardStepBar.js
rename to Components/WizardSteps/WizardStepBar/WizardStepBar.tsx
index 757cbe5..a6adb7c 100644
--- a/Components/WizardSteps/WizardStepBar/WizardStepBar.js
+++ b/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx
@@ -1,12 +1,17 @@
-//Lib
import React from 'react';
import classes from './WizardStepBar.module.css';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
-function WizardStepBar(props) {
- ////Functions
+type WizardStepBarProps = {
+ step: number;
+ setStep: (step: number) => void;
+ previousStepHandler: () => void;
+ nextStepHandler: () => void;
+};
+
+function WizardStepBar(props: WizardStepBarProps) {
//Color onClick on a step
- const colorHandler = (step) => {
+ const colorHandler = (step: number) => {
if (step <= props.step) {
return classes.active;
} else {
diff --git a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx
similarity index 79%
rename from Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js
rename to Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx
index 2443865..3a9bd86 100644
--- a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js
+++ b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx
@@ -1,4 +1,3 @@
-//Lib
import {
Chart as ChartJS,
CategoryScale,
@@ -10,16 +9,15 @@ import {
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import { useState, useEffect } from 'react';
+import { Repository, Optional } from '~/types';
export default function StorageUsedChartBar() {
- //States
- const [data, setData] = useState([]);
+ const [data, setData] = useState>>();
- //LifeCycle
useEffect(() => {
const dataFetch = async () => {
try {
- const response = await fetch('/api/repo', {
+ const response = await fetch('/api/v1/repositories', {
method: 'GET',
headers: {
'Content-type': 'application/json',
@@ -41,10 +39,10 @@ export default function StorageUsedChartBar() {
responsive: true,
plugins: {
legend: {
- position: 'bottom',
+ position: 'bottom' as const,
},
title: {
- position: 'bottom',
+ position: 'bottom' as const,
display: true,
text: 'Storage used for each repository',
},
@@ -55,7 +53,7 @@ export default function StorageUsedChartBar() {
min: 0,
ticks: {
// Include a dollar sign in the ticks
- callback: function (value) {
+ callback: function (value: number | string) {
return value + '%';
},
stepSize: 10,
@@ -64,7 +62,7 @@ export default function StorageUsedChartBar() {
},
};
- const labels = data.map((repo) => repo.alias);
+ const labels = data?.map((repo) => repo.alias);
const dataChart = {
labels,
@@ -72,7 +70,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
deleted file mode 100644
index 276eda6..0000000
--- a/Containers/RepoList/RepoList.js
+++ /dev/null
@@ -1,159 +0,0 @@
-//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
-
-
-
-
- {displayRepoAdd ? (
-
- ) : null}
- {displayRepoEdit ? (
-
- ) : null}
- >
- );
-}
diff --git a/Containers/RepoList/RepoList.module.css b/Containers/RepoList/RepoList.module.css
index 11ff66b..087d90b 100644
--- a/Containers/RepoList/RepoList.module.css
+++ b/Containers/RepoList/RepoList.module.css
@@ -86,7 +86,6 @@
flex-direction: column;
width: 90%;
margin: 5px auto;
- padding: 15px;
}
.unfoldButton {
@@ -123,3 +122,77 @@
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
new file mode 100644
index 0000000..d9160f0
--- /dev/null
+++ b/Containers/RepoList/RepoList.tsx
@@ -0,0 +1,279 @@
+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 && (
+
+ handleSearchChange({
+ target: { value: '' },
+ } as React.ChangeEvent)
+ }
+ className={classes.clearButton}
+ title='Clear search'
+ >
+
+
+ )}
+
+
+
+ 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'
+ />
+
+
+
+
+
+
+ {displayRepoAdd && (
+
+ )}
+ {displayRepoEdit && (
+
+ )}
+ >
+ );
+}
diff --git a/Containers/RepoManage/RepoManage.module.css b/Containers/RepoManage/RepoManage.module.css
index 94e4348..8c55d5c 100644
--- a/Containers/RepoManage/RepoManage.module.css
+++ b/Containers/RepoManage/RepoManage.module.css
@@ -11,7 +11,7 @@
.modale {
position: fixed;
top: 10%;
- width: 1000px;
+ width: 800px;
height: auto;
max-width: 75%;
max-height: 85%;
@@ -24,6 +24,11 @@
animation: append-animate 0.3s linear;
}
+.modale h2 {
+ margin-top: 0;
+ color: #374151;
+}
+
@keyframes append-animate {
from {
transform: scale(0);
@@ -47,89 +52,98 @@
.repoManageForm {
margin: auto;
- width: 80%;
- padding: 15px 30px 30px 30px;
- border-radius: 5px;
- text-align: left;
+ width: 100%;
+ max-width: 600px;
+ border-radius: 8px;
+ background-color: #ffffff;
+ font-family: Inter, sans-serif;
+ color: #1f2937;
}
.formWrapper {
text-align: center;
- margin: auto;
width: 100%;
- height: auto;
- color: #494b7a;
+ margin: 0 auto;
+ color: inherit;
}
.repoManageForm label {
display: block;
- margin-bottom: 8px;
- text-align: center;
- margin-top: 20px;
- color: #494b7a;
+ margin-top: 0.9rem;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ font-size: 0.95rem;
+ color: #374151;
+ text-align: left;
}
.repoManageForm input,
.repoManageForm textarea,
.repoManageForm select {
- border: 1px solid #6d4aff21;
- font-size: 16px;
- height: auto;
- margin: 0;
- margin-bottom: 0px;
- outline: 0;
- padding: 15px;
width: 100%;
- background-color: #f5f5f5;
- border-radius: 5px;
- /* color: #1b1340; */
- color: #494b7a;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
+ padding: 0.75rem 1rem;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ background-color: #f9fafb;
+ color: #111827;
+ font-family: Inter;
}
.repoManageForm textarea {
resize: vertical;
+ min-height: 80px;
}
-.repoManageForm textarea:focus,
.repoManageForm input:focus,
+.repoManageForm textarea:focus,
.repoManageForm select:focus {
- outline: 1px solid #6d4aff;
- box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
+ border-color: #6d4aff;
+ background-color: #ffffff;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.3);
}
.repoManageForm .invalid {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
+ background-color: #fef2f2;
+ border-color: #ef4444;
}
.repoManageForm .invalid:focus {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
- box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
}
.repoManageForm button {
display: block;
- margin: 15px auto;
+ margin: 2rem auto 0 auto;
+ background-color: #6d4aff;
+ color: #ffffff;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 6px;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
}
+
.repoManageForm button:hover {
- display: block;
- margin: 15px auto;
+ background-color: #5c3dff;
}
.errorMessage {
- color: red;
- display: block;
- margin-top: 3px;
+ color: #dc2626;
+ font-size: 0.875rem;
+ margin-top: 0.3rem;
}
.optionCommandWrapper {
display: flex;
- margin-top: 20px;
- color: #494b7a;
+ gap: 0.5rem;
+ align-items: center;
+ margin-top: 1.5rem;
+ font-size: 0.95rem;
+ color: #374151;
}
.optionCommandWrapper label {
@@ -137,15 +151,33 @@
}
.optionCommandWrapper input[type='checkbox'] {
- width: auto;
- margin-right: 8px;
+ width: 18px;
+ height: 18px;
+ accent-color: #6d4aff;
cursor: pointer;
- accent-color: #6d4aff;
}
+
.optionCommandWrapper input[type='checkbox']:focus {
- outline: 0;
- box-shadow: none;
- accent-color: #6d4aff;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.4);
+}
+
+.selectAlert {
+ max-width: 160px;
+}
+.selectAlertWrapper label {
+ margin: 0;
+}
+
+.selectAlertWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 1.5rem;
+ gap: 0.5rem;
+ font-size: 0.95rem;
+ color: #374151;
}
/* DELETE DIALOG */
@@ -254,6 +286,7 @@
}
.littleDeleteButton {
+ margin-top: 10px;
border: none;
font-weight: 300;
color: red;
@@ -261,8 +294,3 @@
background: none;
cursor: pointer;
}
-
-.selectAlert {
- margin: auto auto 35px auto;
- max-width: 160px;
-}
diff --git a/Containers/RepoManage/RepoManage.js b/Containers/RepoManage/RepoManage.tsx
similarity index 52%
rename from Containers/RepoManage/RepoManage.js
rename to Containers/RepoManage/RepoManage.tsx
index a758efa..b37f0cd 100644
--- a/Containers/RepoManage/RepoManage.js
+++ b/Containers/RepoManage/RepoManage.tsx
@@ -1,29 +1,46 @@
-//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 { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react';
import Link from 'next/link';
-import { IconExternalLink } from '@tabler/icons-react';
-import { alertOptions } from '../../domain/constants';
+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';
+import classes from './RepoManage.module.css';
-export default function RepoManage(props) {
- ////Var
- let targetRepo;
+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) {
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 = {
+ const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
@@ -33,108 +50,113 @@ export default function RepoManage(props) {
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 (!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) {
+ if (props.mode === 'edit') {
+ if (!router.query.slug) {
+ start();
+ return;
+ } else if (!targetRepo) {
+ stop();
router.push('/404');
- return null;
}
}
//Delete a repo
- const deleteHandler = async () => {
+ const deleteHandler = async (repositoryName?: string) => {
+ start();
+ if (!repositoryName) {
+ stop();
+ toast.error('Repository name not found', toastOptions);
+ router.replace('/');
+ return;
+ }
//API Call for delete
- fetch('/api/repo/id/' + router.query.slug + '/delete', {
+ await fetch('/api/v1/repositories/' + repositoryName, {
method: 'DELETE',
headers: {
'Content-type': 'application/json',
},
})
- .then((response) => {
+ .then(async (response) => {
if (response.ok) {
toast.success(
- 'π The repository #' + router.query.slug + ' has been successfully deleted',
+ 'π The repository ' + repositoryName + ' 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
);
- else toast.error('An error has occurred', toastOptions);
- router.replace('/');
- console.log('Fail to delete');
+ 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');
+ }
}
})
.catch((error) => {
toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log(error);
+ })
+ .finally(() => {
+ stop();
});
};
- //Verify that the SSH key is unique
- const isSSHKeyUnique = async (sshPublicKey) => {
- let isUnique = true;
+ 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(' ');
- // Extract the first two columns of the SSH key in the form
- const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
+ const response = await fetch('/api/v1/repositories', { method: 'GET' });
+ const data: { repoList: Repository[] } = await response.json();
- 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;
+ 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)
+ );
});
- return isUnique;
+
+ 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;
+ }
};
//Form submit Handler for ADD or EDIT a repo
- const formSubmitHandler = async (dataForm) => {
- //Loading button on submit to avoid multiple send.
+ const formSubmitHandler = async (dataForm: DataForm) => {
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(dataForm.sshkey))) {
+ if (!(await isSSHKeyUnique(cleanedSSHKey))) {
+ stop();
setIsLoading(false);
return;
}
@@ -143,14 +165,14 @@ export default function RepoManage(props) {
const newRepo = {
alias: dataForm.alias,
storageSize: parseInt(dataForm.storageSize),
- sshPublicKey: dataForm.sshkey,
+ sshPublicKey: cleanedSSHKey,
comment: dataForm.comment,
alert: dataForm.alert.value,
lanCommand: dataForm.lanCommand,
appendOnlyMode: dataForm.appendOnlyMode,
};
//POST API to send new repo
- await fetch('/api/repo/add', {
+ await fetch('/api/v1/repositories', {
method: 'POST',
headers: {
'Content-type': 'application/json',
@@ -163,7 +185,7 @@ export default function RepoManage(props) {
router.replace('/');
} else {
const errorMessage = await response.json();
- toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
+ toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
router.replace('/');
console.log(`Fail to ${props.mode}`);
}
@@ -172,19 +194,23 @@ export default function RepoManage(props) {
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: dataForm.sshkey,
+ sshPublicKey: cleanedSSHKey,
comment: dataForm.comment,
alert: dataForm.alert.value,
lanCommand: dataForm.lanCommand,
appendOnlyMode: dataForm.appendOnlyMode,
};
- await fetch('/api/repo/id/' + router.query.slug + '/edit', {
+ await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, {
method: 'PATCH',
headers: {
'Content-type': 'application/json',
@@ -194,13 +220,13 @@ export default function RepoManage(props) {
.then(async (response) => {
if (response.ok) {
toast.success(
- 'The repository #' + targetRepo.id + ' has been successfully edited !',
+ 'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !',
toastOptions
);
router.replace('/');
} else {
const errorMessage = await response.json();
- toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
+ toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
router.replace('/');
console.log(`Fail to ${props.mode}`);
}
@@ -209,6 +235,10 @@ export default function RepoManage(props) {
toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log(error);
+ })
+ .finally(() => {
+ stop();
+ setIsLoading(false);
});
}
};
@@ -231,54 +261,54 @@ export default function RepoManage(props) {
color: 'rgba(99, 115, 129, 0.38)',
}}
>
- #{targetRepo.id}
+ {targetRepo?.repositoryName}
{' '}
?
- You are about to permanently delete the repository #{targetRepo.id} and all
- the backups it contains.
+ You are about to permanently delete the repository{' '}
+ {targetRepo?.repositoryName} and all the backups it contains.
The data will not be recoverable and it will not be possible to go back.
- {isLoading ? (
-
- ) : (
- <>
-
- Cancel
-
- {
- deleteHandler();
- setIsLoading(true);
- }}
- className={classes.deleteButton}
- >
- Yes, delete it !
-
- >
- )}
+ <>
+
+ Cancel
+
+ {
+ deleteHandler(targetRepo?.repositoryName);
+ setIsLoading(true);
+ }}
+ className={classes.deleteButton}
+ >
+ Yes, delete it !
+
+ >
) : (
{props.mode == 'edit' && (
-
+
Edit the repository{' '}
- #{targetRepo.id}
+ {targetRepo?.repositoryName}
-
+
)}
- {props.mode == 'add' &&
Add a repository }
+ {props.mode == 'add' &&
Add a repository }