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 && (
+
+ )}
+
+
+
+
+
+
+ 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 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 (
- <>
-
-
-
- {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.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 (
+
);
}
diff --git a/Components/WizardSteps/WizardStep2/WizardStep2.tsx b/Components/WizardSteps/WizardStep2/WizardStep2.js
similarity index 78%
rename from Components/WizardSteps/WizardStep2/WizardStep2.tsx
rename to Components/WizardSteps/WizardStep2/WizardStep2.js
index e4f760b..d9b634a 100644
--- a/Components/WizardSteps/WizardStep2/WizardStep2.tsx
+++ b/Components/WizardSteps/WizardStep2/WizardStep2.js
@@ -1,14 +1,16 @@
-import { IconAlertCircle, IconTool } from '@tabler/icons-react';
-import CopyButton from '../../UI/CopyButton/CopyButton';
-import { WizardStepProps } from '~/types';
+//Lib
+import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
-import { lanCommandOption } from '~/helpers/functions';
+import { IconTool, IconAlertCircle } from '@tabler/icons-react';
+import CopyButton from '../../UI/CopyButton/CopyButton';
+import lanCommandOption from '../../../helpers/functions/lanCommandOption';
-function WizardStep2(props: WizardStepProps) {
+function WizardStep2(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,10 +32,10 @@ function WizardStep2(props: WizardStepProps) {
borg init -e repokey-blake2 ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
- {props.selectedRepo?.repositoryName}
+ {props.selectedOption.repositoryName}
@@ -69,8 +71,8 @@ function WizardStep2(props: WizardStepProps) {
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 :
@@ -98,21 +100,19 @@ function WizardStep2(props: WizardStepProps) {
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.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
+
+
+
+
+ {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 && (
-
- 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.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.
- <>
-
- Cancel
-
- {
- deleteHandler(targetRepo?.repositoryName);
- setIsLoading(true);
- }}
- className={classes.deleteButton}
- >
- Yes, delete it !
-
- >
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ Cancel
+
+ {
+ deleteHandler();
+ setIsLoading(true);
+ }}
+ className={classes.deleteButton}
+ >
+ Yes, delete it !
+
+ >
+ )}
) : (
{props.mode == 'edit' && (
-
+
Edit the repository{' '}
- {targetRepo?.repositoryName}
+ #{targetRepo.id}
-
+
)}
- {props.mode == 'add' &&
Add a repository }
+ {props.mode == 'add' &&
Add a repository }