mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
918c6c448c | ||
|
|
1e2b4f16b3 | ||
|
|
f0d02eb8b3 | ||
|
|
96274206c5 | ||
|
|
7ae7ea6f79 | ||
|
|
487e7ad83d | ||
|
|
c9dabd39fe | ||
|
|
30ab689c6e | ||
|
|
8143af18af | ||
|
|
471c65cc48 | ||
|
|
9162d5afcc | ||
|
|
9236c664c2 | ||
|
|
767ed69020 | ||
|
|
93c706719f | ||
|
|
ed1ac1926b | ||
|
|
3adb6d01c0 | ||
|
|
9c195c1f30 | ||
|
|
e27093404b | ||
|
|
9602427ca0 | ||
|
|
46812cb5ff | ||
|
|
6258d92464 | ||
|
|
c57d913c46 | ||
|
|
5d285b7ac6 | ||
|
|
c96219f851 | ||
|
|
c7d949b1d1 | ||
|
|
6593a8e324 | ||
|
|
0988fa0115 | ||
|
|
9f0b1a172c | ||
|
|
0126a829dd | ||
|
|
b04a2750a3 | ||
|
|
9e22160b16 | ||
|
|
e8e4b093d2 | ||
|
|
0cb27f2637 | ||
|
|
1497c7385b | ||
|
|
327b06d2a1 | ||
|
|
2c2cfc0b73 | ||
|
|
93327b0a6a | ||
|
|
11c74d4117 | ||
|
|
f93bb6e33e | ||
|
|
73699bbda9 | ||
|
|
5f24ec2d86 | ||
|
|
c08bb8f8e0 | ||
|
|
d01e705866 | ||
|
|
ecc80c3c01 | ||
|
|
784a907384 | ||
|
|
b68a5f32ca | ||
|
|
467f9e2ffd | ||
|
|
1d25acde61 | ||
|
|
cb6ac86bb0 | ||
|
|
cd2da4441d | ||
|
|
d7515b172c | ||
|
|
e4de903cc9 | ||
|
|
45166a3b9c | ||
|
|
687b59e397 | ||
|
|
9a3be8d5a5 | ||
|
|
0c23c24651 | ||
|
|
e20e7f016c | ||
|
|
a0c1a24ba9 | ||
|
|
0475b3d01a | ||
|
|
39cb9b99f6 | ||
|
|
efd123ddfe | ||
|
|
413318886c | ||
|
|
187f9d2386 | ||
|
|
5f61c7baa6 | ||
|
|
e3c5ec9a58 | ||
|
|
fde52415cb | ||
|
|
d92225d093 | ||
|
|
e2a0932229 | ||
|
|
6acdf73557 | ||
|
|
4405b61e03 | ||
|
|
fba714d679 | ||
|
|
8e15282405 | ||
|
|
72b5816451 | ||
|
|
4016191497 | ||
|
|
94b36c1622 | ||
|
|
aa736b8b4a | ||
|
|
94cd937c56 | ||
|
|
9ccf3b7242 | ||
|
|
01b7a58c75 | ||
|
|
2b6c661fea | ||
|
|
fbd61cdf2f | ||
|
|
e9534181da | ||
|
|
fdc613fe8e | ||
|
|
fca09a84cb | ||
|
|
6ec6fb1f11 | ||
|
|
3d51d3548f | ||
|
|
8479b6c822 | ||
|
|
21bb165103 | ||
|
|
c7ca01c0fb | ||
|
|
5d2172f2bf | ||
|
|
1decd9d7a5 | ||
|
|
d0091cfed3 | ||
|
|
0ebcf1729c | ||
|
|
51d101e8f0 | ||
|
|
2d9bf225a0 | ||
|
|
f85d12448c | ||
|
|
82c8ad98b4 | ||
|
|
2499c7c426 | ||
|
|
da530217ab | ||
|
|
4c70d8aac7 | ||
|
|
ba1dbcc9f3 | ||
|
|
0ad380cc81 | ||
|
|
b77a2e374e | ||
|
|
647108e8ee | ||
|
|
8e4e41ca07 | ||
|
|
a8eb3f5f1a | ||
|
|
eb5ac5c8ea | ||
|
|
29344e0c48 | ||
|
|
73f7ac057c | ||
|
|
d359d53fb7 | ||
|
|
ff83ee9c62 | ||
|
|
da3a8d7fbe | ||
|
|
ba2fc55188 | ||
|
|
a6896c9b50 | ||
|
|
f2563c5e8d | ||
|
|
9326291780 | ||
|
|
8dd74f310d | ||
|
|
44ed9e9ca6 | ||
|
|
ab22680625 | ||
|
|
a73e29efc9 | ||
|
|
26273b7240 | ||
|
|
c925407f05 | ||
|
|
622fc369d0 | ||
|
|
0c4b7573cc | ||
|
|
401ce5c290 | ||
|
|
9580ad3934 | ||
|
|
b33c0810d5 | ||
|
|
e3e84e0d18 | ||
|
|
55cb4a7144 | ||
|
|
8fbe639aa1 | ||
|
|
9bc042e2cd | ||
|
|
67302c6a17 | ||
|
|
d7e2ec3b08 | ||
|
|
fdaca0e8a7 | ||
|
|
3af5f89606 | ||
|
|
3ccccfc5e5 | ||
|
|
7959f903bb | ||
|
|
59eb91f28f | ||
|
|
9aecfb22a5 | ||
|
|
ba4476a3a7 | ||
|
|
64724d2a92 | ||
|
|
be58ebdb7d | ||
|
|
746a9f5eb6 | ||
|
|
91a6d672fa | ||
|
|
50809476fa | ||
|
|
483f9cf192 | ||
|
|
f1c7dcda45 | ||
|
|
282ffb543c | ||
|
|
c840196a14 | ||
|
|
7d76037a43 | ||
|
|
68fea480ae | ||
|
|
b063e5c276 | ||
|
|
f2d98505f1 | ||
|
|
ec64ea8229 | ||
|
|
59dc181625 | ||
|
|
79387b83f6 | ||
|
|
b13433e554 | ||
|
|
d9fd382cb2 | ||
|
|
baa5e300c8 | ||
|
|
51747773ba | ||
|
|
4d324d885d | ||
|
|
af1a437523 | ||
|
|
dd8663591e | ||
|
|
de4f31f255 | ||
|
|
53c1bc9e59 | ||
|
|
77901ae1bc | ||
|
|
1aeff542e5 | ||
|
|
c05469d704 | ||
|
|
1e6bb542f9 | ||
|
|
b81ad079ee | ||
|
|
044edcda07 | ||
|
|
7eb85d2a99 | ||
|
|
7d4d6cd9df | ||
|
|
3f7eb4528e | ||
|
|
0008f70552 | ||
|
|
ff48517b43 | ||
|
|
2695715180 | ||
|
|
b3be1a561c | ||
|
|
cab7a7f8bb | ||
|
|
f6da5961c3 | ||
|
|
d297f1a27b | ||
|
|
b513509b01 | ||
|
|
bfa3f7e04e | ||
|
|
539b516e45 | ||
|
|
25744efc66 | ||
|
|
21e37366fb | ||
|
|
6002ad44e1 | ||
|
|
a431270718 | ||
|
|
3f6a47c39d | ||
|
|
4dace0a32b | ||
|
|
efb5318fc5 | ||
|
|
a5e83f3133 | ||
|
|
7b6ca12744 | ||
|
|
da1b5a969c | ||
|
|
5d790231ea | ||
|
|
ad363ce38c | ||
|
|
8e5d6eec6c | ||
|
|
92f1400a35 | ||
|
|
47f1938136 | ||
|
|
13d3cfb290 | ||
|
|
c657bdfb63 | ||
|
|
f226218b3a | ||
|
|
8f7fa0dea1 | ||
|
|
292b3c805d | ||
|
|
ded26f92af | ||
|
|
8cce7bbaf4 | ||
|
|
228b8c3ec7 | ||
|
|
2e298ce757 | ||
|
|
81daa9f0b0 | ||
|
|
713c8d9177 | ||
|
|
f571162eed | ||
|
|
19d2872943 | ||
|
|
0752b8bf7f | ||
|
|
c01097196a | ||
|
|
5f1bee018d | ||
|
|
8655b81618 | ||
|
|
1702e43373 | ||
|
|
a7d411f6ff | ||
|
|
6a84da0f96 | ||
|
|
79425f4490 | ||
|
|
2fcddea0fa | ||
|
|
ac1c8552e1 | ||
|
|
6a18eefe34 | ||
|
|
a43eedaf9f | ||
|
|
77bc24990e | ||
|
|
edbc79c89e | ||
|
|
527a090f1b | ||
|
|
370b86337f | ||
|
|
1ce3acdde0 | ||
|
|
1a72be323b | ||
|
|
bf252f22f2 | ||
|
|
937d92f686 | ||
|
|
5572957731 | ||
|
|
d2db5722b7 | ||
|
|
5404185a6c | ||
|
|
800f2dfbbb | ||
|
|
ed0b5410f6 | ||
|
|
ae72e262aa | ||
|
|
c4c7050c3f | ||
|
|
81e5227270 | ||
|
|
39319d9162 | ||
|
|
12206fa856 | ||
|
|
9d8c6e26f1 | ||
|
|
eb624672e6 | ||
|
|
1b026b5f08 | ||
|
|
21b9b8b44d | ||
|
|
6ef7fcc0c2 | ||
|
|
fc4ce178cb | ||
|
|
98b72988c4 | ||
|
|
317473e528 | ||
|
|
e13c5ddf51 | ||
|
|
90be3ab731 | ||
|
|
85c0445a64 | ||
|
|
6a227ccd7c | ||
|
|
3d9a21f854 | ||
|
|
d4229a985d | ||
|
|
c6e0921dea | ||
|
|
346a8a4375 | ||
|
|
4799faa827 | ||
|
|
2c6f5c38ed | ||
|
|
4a884ab5cc | ||
|
|
e59beaf749 | ||
|
|
0d4e1229ab | ||
|
|
8810fd91c9 | ||
|
|
095444610d | ||
|
|
7cf43c5e7c | ||
|
|
77183af08d | ||
|
|
3a2c5be5dd | ||
|
|
11c764c2f3 | ||
|
|
d128234ae6 | ||
|
|
ee31ee225d | ||
|
|
e2195616d3 | ||
|
|
2ca818b996 | ||
|
|
131f2b4538 | ||
|
|
6731c3fea9 | ||
|
|
d3da5e8c13 | ||
|
|
3b0d4225ce | ||
|
|
66f0930164 | ||
|
|
3979b83a0a | ||
|
|
b65e432564 | ||
|
|
340e7c3e00 | ||
|
|
dcdce63e15 | ||
|
|
c2eaed757d | ||
|
|
920743bea7 | ||
|
|
0970f5baae | ||
|
|
291aa13e9a | ||
|
|
e644cde514 | ||
|
|
2350e68f4f | ||
|
|
cce3b681e3 | ||
|
|
755799f19d | ||
|
|
ec02dc8579 | ||
|
|
7028982ba1 | ||
|
|
f04503e38e | ||
|
|
f1edffed17 | ||
|
|
fac87acee7 | ||
|
|
c46dd91ede | ||
|
|
648753f8cf | ||
|
|
76b3bda313 | ||
|
|
8102dd1a8f | ||
|
|
aceb4e7f4c | ||
|
|
4224389e96 | ||
|
|
7c66c10b6d | ||
|
|
bf06a2e50c | ||
|
|
8c69fdef95 | ||
|
|
883c1d2f57 | ||
|
|
e7f1652b4e | ||
|
|
ea07224a52 |
@@ -35,6 +35,11 @@ AUTH_OIDC_DISCOVERY_ENDPOINT=""
|
|||||||
AUTH_OIDC_JWKS_ENDPOINT=""
|
AUTH_OIDC_JWKS_ENDPOINT=""
|
||||||
AUTH_OIDC_PKCE=true
|
AUTH_OIDC_PKCE=true
|
||||||
ALLOWED_GROUP="admin"
|
ALLOWED_GROUP="admin"
|
||||||
|
AUTH_OIDC_ROLE_MAP=""
|
||||||
|
|
||||||
|
AUTH_ALLOW_LINKING=true
|
||||||
|
AUTH_ALLOW_UNLINKING=true
|
||||||
|
|
||||||
|
|
||||||
# Social OAuth2 Authentification
|
# Social OAuth2 Authentification
|
||||||
AUTH_SOCIAL_ID=""
|
AUTH_SOCIAL_ID=""
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
|
|||||||
|
|
||||||
3. **Push your branch**
|
3. **Push your branch**
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/<feature-name>
|
git push origin feat/<feature-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Open a Pull Request (PR)**
|
4. **Open a Pull Request (PR)**
|
||||||
@@ -82,7 +82,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
|
|||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8).
|
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8,Biome).
|
||||||
- Use meaningful variable names and include comments where necessary.
|
- Use meaningful variable names and include comments where necessary.
|
||||||
- Tests before submitting your changes.
|
- Tests before submitting your changes.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Discord Notification
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
discord_title:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
discord_color:
|
||||||
|
required: true
|
||||||
|
type: number
|
||||||
|
discord_footer:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
DISCORD_WEBHOOK:
|
||||||
|
required: true
|
||||||
|
GH_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify-discord:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Send Discord Notification
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_INFO=$(gh release view "${{ inputs.release_tag }}" -R ${{ github.repository }} --json name,url,body,author)
|
||||||
|
|
||||||
|
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
||||||
|
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ inputs.release_tag }}"; fi
|
||||||
|
|
||||||
|
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
||||||
|
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
||||||
|
|
||||||
|
AUTHOR_NAME="Portabase"
|
||||||
|
AUTHOR_ICON="https://github.com/Portabase.png"
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg title "$RELEASE_TITLE" \
|
||||||
|
--arg description "$RELEASE_BODY" \
|
||||||
|
--arg url "$RELEASE_URL" \
|
||||||
|
--arg author "$AUTHOR_NAME" \
|
||||||
|
--arg icon "$AUTHOR_ICON" \
|
||||||
|
--arg discord_title "${{ inputs.discord_title }}" \
|
||||||
|
--arg discord_footer "${{ inputs.discord_footer }}" \
|
||||||
|
--argjson discord_color ${{ inputs.discord_color }} \
|
||||||
|
'{
|
||||||
|
content: $discord_title,
|
||||||
|
embeds: [{
|
||||||
|
title: $title,
|
||||||
|
url: $url,
|
||||||
|
description: $description,
|
||||||
|
color: $discord_color,
|
||||||
|
author: {
|
||||||
|
name: $author,
|
||||||
|
icon_url: $icon
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
text: $discord_footer
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"$DISCORD_WEBHOOK"
|
||||||
@@ -3,10 +3,13 @@ name: Docker Publish
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
image_name:
|
image_name:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'portabase/portabase'
|
default: "portabase/portabase"
|
||||||
add_latest:
|
add_latest:
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -14,11 +17,11 @@ on:
|
|||||||
target:
|
target:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'prod'
|
default: "prod"
|
||||||
dockerfile:
|
dockerfile:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: './docker/dockerfile/Dockerfile'
|
default: "./docker/dockerfile/Dockerfile"
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME:
|
DOCKER_USERNAME:
|
||||||
required: true
|
required: true
|
||||||
@@ -26,10 +29,9 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build and push Docker images
|
name: Build architectures
|
||||||
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
|
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -38,22 +40,22 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Set image tag
|
- name: Prepare Image Tags
|
||||||
id: set-tags
|
id: prep
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||||
if [ "${{ matrix.platform }}" = "linux/amd64" ]; then
|
echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT
|
||||||
IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64"
|
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
|
||||||
else
|
|
||||||
IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64"
|
|
||||||
fi
|
|
||||||
echo "image=$IMAGE" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -62,61 +64,73 @@ jobs:
|
|||||||
file: ${{ inputs.dockerfile }}
|
file: ${{ inputs.dockerfile }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.set-tags.outputs.image }}
|
tags: ${{ steps.prep.outputs.image }}
|
||||||
target: ${{ inputs.target }}
|
target: ${{ inputs.target }}
|
||||||
|
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||||
|
|
||||||
- name: Prepare artifact name
|
- name: Save image name for manifest
|
||||||
id: artifact
|
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||||
run: |
|
|
||||||
platform=${{ matrix.platform }}
|
|
||||||
echo "safe_platform=${platform//\//-}" >> $GITHUB_OUTPUT
|
|
||||||
echo "${{ steps.set-tags.outputs.image }}" > image.txt
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-${{ steps.artifact.outputs.safe_platform }}
|
name: image-${{ steps.prep.outputs.safe_platform }}
|
||||||
path: image.txt
|
path: image.txt
|
||||||
if-no-files-found: warn
|
retention-days: 1
|
||||||
compression-level: 6
|
|
||||||
overwrite: false
|
|
||||||
include-hidden-files: false
|
|
||||||
|
|
||||||
create-manifest:
|
create-manifest:
|
||||||
name: Create multi-arch Docker manifest (Portabase)
|
name: Create multi-arch manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-linux-amd64
|
name: image-linux-amd64
|
||||||
path: /tmp/digests/amd64
|
path: /tmp/digests/amd64
|
||||||
|
|
||||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-linux-arm64
|
name: image-linux-arm64
|
||||||
path: /tmp/digests/arm64
|
path: /tmp/digests/arm64
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Generate semantic tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
|
||||||
|
|
||||||
|
echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
if [ -n "$MINOR" ]; then
|
||||||
|
echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -n "$MAJOR" ]; then
|
||||||
|
echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
||||||
|
echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
|
||||||
|
|
||||||
- name: Create and push manifest list
|
- name: Create and push manifest list
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
||||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||||
MANIFEST_IMAGE="${{ inputs.image_name }}:$REF_NAME"
|
echo $TAG_ARGS
|
||||||
|
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
|
||||||
docker buildx imagetools create $DOCKER_IMAGES -t $MANIFEST_IMAGE
|
|
||||||
docker buildx imagetools inspect $MANIFEST_IMAGE
|
|
||||||
|
|
||||||
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
|
||||||
docker buildx imagetools create $DOCKER_IMAGES -t ${{ inputs.image_name }}:latest
|
|
||||||
docker buildx imagetools inspect ${{ inputs.image_name }}:latest
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
name: GitHub Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
prerelease:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
make_latest:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
discord_title:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
discord_color:
|
|
||||||
required: true
|
|
||||||
type: number
|
|
||||||
discord_footer:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
secrets:
|
|
||||||
DISCORD_WEBHOOK:
|
|
||||||
required: true
|
|
||||||
GH_TOKEN:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Build Changelog
|
|
||||||
id: build_changelog
|
|
||||||
uses: mikepenz/release-changelog-builder-action@v5
|
|
||||||
with:
|
|
||||||
mode: "COMMIT"
|
|
||||||
configurationJson: |
|
|
||||||
{
|
|
||||||
"template": "#{{CHANGELOG}}",
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"title": "## Feature",
|
|
||||||
"labels": ["feat", "feature"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "## Fix",
|
|
||||||
"labels": ["fix", "bug"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "## Other",
|
|
||||||
"labels": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"label_extractor": [
|
|
||||||
{
|
|
||||||
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
|
|
||||||
"on_property": "title",
|
|
||||||
"target": "$1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
generate_release_notes: false
|
|
||||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
|
||||||
prerelease: ${{ inputs.prerelease }}
|
|
||||||
make_latest: ${{ inputs.make_latest }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Send Discord Notification
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
|
|
||||||
|
|
||||||
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
|
||||||
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
|
|
||||||
|
|
||||||
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
|
||||||
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
|
||||||
|
|
||||||
AUTHOR_NAME="Portabase"
|
|
||||||
AUTHOR_ICON="https://github.com/Portabase.png"
|
|
||||||
|
|
||||||
PAYLOAD=$(jq -n \
|
|
||||||
--arg title "$RELEASE_TITLE" \
|
|
||||||
--arg description "$RELEASE_BODY" \
|
|
||||||
--arg url "$RELEASE_URL" \
|
|
||||||
--arg author "$AUTHOR_NAME" \
|
|
||||||
--arg icon "$AUTHOR_ICON" \
|
|
||||||
--arg discord_title "${{ inputs.discord_title }}" \
|
|
||||||
--arg discord_footer "${{ inputs.discord_footer }}" \
|
|
||||||
--argjson discord_color ${{ inputs.discord_color }} \
|
|
||||||
'{
|
|
||||||
content: $discord_title,
|
|
||||||
embeds: [{
|
|
||||||
title: $title,
|
|
||||||
url: $url,
|
|
||||||
description: $description,
|
|
||||||
color: $discord_color,
|
|
||||||
author: {
|
|
||||||
name: $author,
|
|
||||||
icon_url: $icon
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
text: $discord_footer
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}'
|
|
||||||
)
|
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" \
|
|
||||||
-d "$PAYLOAD" \
|
|
||||||
"$DISCORD_WEBHOOK"
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Publish Helm Chart
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GH_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-helm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
|
- name: Package Helm chart
|
||||||
|
run: |
|
||||||
|
mkdir -p ./helm-packages
|
||||||
|
helm package helm \
|
||||||
|
--version ${{ inputs.version }} \
|
||||||
|
--app-version ${{ inputs.version }} \
|
||||||
|
--destination ./helm-packages
|
||||||
|
|
||||||
|
- name: Authenticate to GitHub Packages
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GH_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
- name: Push Helm chart to GitHub Packages (OCI)
|
||||||
|
run: |
|
||||||
|
helm push ./helm-packages/portabase-${{ inputs.version }}.tgz oci://ghcr.io/portabase/charts
|
||||||
@@ -3,29 +3,28 @@ name: Publish Docker image for release candidate
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*-rc*'
|
- "*.*.*-rc*"
|
||||||
|
- "!*-*-rc*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_publish:
|
docker_publish:
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
add_latest: false
|
add_latest: true
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
github_release:
|
discord_notification:
|
||||||
needs: docker_publish
|
needs: docker_publish
|
||||||
uses: ./.github/workflows/github.yml
|
uses: ./.github/workflows/discord.yml
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
discord_title: "New Release Candidate: ${{ github.ref_name }}"
|
||||||
make_latest: false
|
discord_color: 2044800
|
||||||
discord_title: "||@release-dashboard|| New release candidate published"
|
|
||||||
discord_color: 16776960
|
|
||||||
discord_footer: "Portabase"
|
discord_footer: "Portabase"
|
||||||
secrets:
|
secrets:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
+100
-13
@@ -1,32 +1,119 @@
|
|||||||
name: Publish Docker image for release
|
name: Auto Release & Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
tags:
|
types: [ closed ]
|
||||||
- '*.*.*'
|
branches:
|
||||||
- '!*-*'
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_publish:
|
create-release:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
|
||||||
|
version: ${{ steps.release_step.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
git config --global user.name 'github-actions[bot]'
|
||||||
|
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
|
|
||||||
|
- name: Run release-it
|
||||||
|
id: release_step
|
||||||
|
run: |
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
VERSION=$(pnpm exec release-it --ci --release-version)
|
||||||
|
echo $VERSION
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
OUTPUT=$(pnpm exec release-it --ci)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
|
||||||
|
DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+')
|
||||||
|
echo $DRAFT_TAG
|
||||||
|
echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
needs: create-release
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
|
version: ${{ needs.create-release.outputs.version }}
|
||||||
add_latest: true
|
add_latest: true
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
github_release:
|
publish-helm:
|
||||||
needs: docker_publish
|
needs: create-release
|
||||||
uses: ./.github/workflows/github.yml
|
uses: ./.github/workflows/helm.yml
|
||||||
with:
|
with:
|
||||||
prerelease: false
|
version: ${{ needs.create-release.outputs.version }}
|
||||||
make_latest: true
|
secrets:
|
||||||
discord_title: "||@release-dashboard|| New release published"
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
discord_color: 5814783
|
|
||||||
|
finalize-release:
|
||||||
|
needs:
|
||||||
|
- create-release
|
||||||
|
- publish-docker
|
||||||
|
- publish-helm
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Publish GitHub Release
|
||||||
|
id: publish_release_step
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||')
|
||||||
|
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
notify-discord:
|
||||||
|
needs:
|
||||||
|
- publish-docker
|
||||||
|
- publish-helm
|
||||||
|
- create-release
|
||||||
|
- finalize-release
|
||||||
|
uses: ./.github/workflows/discord.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ needs.create-release.outputs.version }}
|
||||||
|
discord_title: "New Release"
|
||||||
|
discord_color: 3066993
|
||||||
discord_footer: "Portabase"
|
discord_footer: "Portabase"
|
||||||
secrets:
|
secrets:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sca-deps: # Dependency & container scan
|
sca-deps:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -17,15 +17,16 @@ jobs:
|
|||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
secrets-gitleaks: # Secrets exposure
|
secrets-gitleaks:
|
||||||
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full history for gitleaks
|
fetch-depth: 0
|
||||||
- uses: gitleaks/gitleaks-action@v2
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
with:
|
with:
|
||||||
config-path: .gitleaks.toml # Optional, if you have a custom config
|
config-path: .gitleaks.toml
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"draft": true,
|
||||||
|
"tokenRef": "GITHUB_TOKEN"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"commit": true,
|
||||||
|
"commitMessage": "chore: release ${version}",
|
||||||
|
"requireCleanWorkingDir": true,
|
||||||
|
"tag": true,
|
||||||
|
"tagName": "${version}",
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/conventional-changelog": {
|
||||||
|
"preset": {
|
||||||
|
"name": "conventionalcommits",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "feat",
|
||||||
|
"section": "✨ Features"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fix",
|
||||||
|
"section": "🐛 Bug Fixes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "perf",
|
||||||
|
"section": "⚡️ Performance Improvements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "revert",
|
||||||
|
"section": "⏪️ Reverts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "docs",
|
||||||
|
"section": "📝 Documentation",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"section": "🔧 Chores",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@release-it/bumper": {
|
||||||
|
"out": {
|
||||||
|
"file": "CITATION.cff",
|
||||||
|
"path": "version",
|
||||||
|
"type": "text/yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-4
@@ -1,6 +1,6 @@
|
|||||||
cff-version: 1.2.0
|
cff-version: 1.2.0
|
||||||
title: Portabase
|
title: Portabase
|
||||||
message: "If you use this software, please cite it as below."
|
message: If you use this software, please cite it as below.
|
||||||
type: software
|
type: software
|
||||||
authors:
|
authors:
|
||||||
- family-names: Gauthereau
|
- family-names: Gauthereau
|
||||||
@@ -9,7 +9,12 @@ authors:
|
|||||||
given-names: Killian
|
given-names: Killian
|
||||||
repository-code: https://github.com/Portabase/portabase
|
repository-code: https://github.com/Portabase/portabase
|
||||||
url: https://portabase.io
|
url: https://portabase.io
|
||||||
abstract: "Portabase is a free, open-source, self-hosted solution for database administration, providing backup and restore capabilities, scheduling, retention policies, notifications, and support for multiple storage backends. Its headless agent architecture enables connection to multiple database instances securely and efficiently."
|
abstract: >-
|
||||||
|
Portabase is a free, open-source, self-hosted solution for database
|
||||||
|
administration, providing backup and restore capabilities, scheduling,
|
||||||
|
retention policies, notifications, and support for multiple storage backends.
|
||||||
|
Its headless agent architecture enables connection to multiple database
|
||||||
|
instances securely and efficiently.
|
||||||
keywords:
|
keywords:
|
||||||
- docker
|
- docker
|
||||||
- kubernetes
|
- kubernetes
|
||||||
@@ -26,5 +31,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.4.0
|
version: 1.5.3
|
||||||
date-released: "2026-02-27"
|
date-released: '2026-03-02'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://www.mysql.com/)
|
[](https://www.mysql.com/)
|
||||||
[](https://mariadb.org/)
|
[](https://mariadb.org/)
|
||||||
|
[](https://sqlite.org/)
|
||||||
[](https://www.mongodb.com/)
|
[](https://www.mongodb.com/)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
import { PageParams } from "@/types/next";
|
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
|
|
||||||
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
|
||||||
const { databaseId } = await props.params;
|
|
||||||
|
|
||||||
const dbItem = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dbItem) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Edit {dbItem.name}</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<DatabaseForm
|
|
||||||
databaseId={databaseId}
|
|
||||||
// @ts-ignore
|
|
||||||
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
Page,
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
PageActions,
|
||||||
import {notFound} from "next/navigation";
|
PageContent,
|
||||||
import {Metadata} from "next";
|
PageHeader,
|
||||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
PageTitle,
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
} from "@/features/layout/page";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
import { notFound } from "next/navigation";
|
||||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
import { Metadata } from "next";
|
||||||
import {Button} from "@/components/ui/button";
|
import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import { getOrganizationChannels } from "@/db/services/notification-channel";
|
||||||
import {db} from "@/db";
|
import { computeOrganizationPermissions } from "@/lib/acl/organization-acl";
|
||||||
import {isNull} from "drizzle-orm";
|
import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
|
||||||
|
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||||
|
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { isNull } from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@@ -24,18 +28,18 @@ export const metadata: Metadata = {
|
|||||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization || !activeMember || !user) {
|
if (!organization || !activeMember || !user) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationChannels = await getOrganizationChannels(organization.id)
|
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||||
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||||
const permissions = computeOrganizationPermissions(activeMember);
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const users = await db.query.user.findMany({
|
||||||
where: (fields) => isNull(fields.deletedAt)
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
const organizationWithMembers = await db.query.organization.findFirst({
|
const organizationWithMembers = await db.query.organization.findFirst({
|
||||||
@@ -43,10 +47,10 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
with: {
|
with: {
|
||||||
members: {
|
members: {
|
||||||
with: {
|
with: {
|
||||||
user: true
|
user: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!organizationWithMembers) notFound();
|
if (!organizationWithMembers) notFound();
|
||||||
@@ -55,12 +59,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
<div className="min-w-full md:min-w-fit ">
|
<div className="min-w-full md:min-w-fit ">Organization settings</div>
|
||||||
Organization settings
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
{permissions.canManageSettings &&
|
||||||
|
organization.slug !== "default" && (
|
||||||
<EditOrganizationDialog
|
<EditOrganizationDialog
|
||||||
organization={organizationWithMembers}
|
organization={organizationWithMembers}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -69,8 +72,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
{permissions.canManageDangerZone &&
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
organization.slug !== "default" && (
|
||||||
|
<DeleteOrganizationButton
|
||||||
|
organizationSlug={organization.slug}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,5 +91,5 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const body: BodyPatch = await request.json();
|
const body: BodyPatch = await request.json();
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
const status = body.status
|
const status = body.status
|
||||||
const backupId = body.backupId
|
const backupId = body.backupId
|
||||||
@@ -131,7 +132,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
await sendNotificationsBackupRestore(database, `${status}_backup` as EventKind);
|
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export async function POST(
|
|||||||
.set({status: body.status as RestorationStatus})
|
.set({status: body.status as RestorationStatus})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
|
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
||||||
await sendNotificationsBackupRestore(database, body.status == "error" ? "error_restore" : "success_restore");
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
message: true,
|
message: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
# context: .
|
# context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
# target: prod
|
||||||
image: portabase/portabase:1.2.7-rc.1
|
image: portabase/portabase:1.4.0
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#FROM node:22-bullseye AS base
|
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
@@ -112,8 +111,6 @@ RUN chmod +x /usr/local/bin/tusd
|
|||||||
|
|
||||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
#RUN mkdir -p .next /app/private/uploads \
|
|
||||||
# && chown -R nextjs:nodejs .next /app/private /app/public
|
|
||||||
|
|
||||||
RUN mkdir -p .next /data/private/uploads \
|
RUN mkdir -p .next /data/private/uploads \
|
||||||
&& chown -R nextjs:nodejs .next /data/private /app/public
|
&& chown -R nextjs:nodejs .next /data/private /app/public
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.DS_Store
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: portabase
|
||||||
|
description: Helm chart for Portabase
|
||||||
|
type: application
|
||||||
|
version: 0.0.0
|
||||||
|
appVersion: "latest"
|
||||||
|
keywords:
|
||||||
|
- postgresql
|
||||||
|
- mariadb
|
||||||
|
- mongodb
|
||||||
|
- mysql
|
||||||
|
- sqlite
|
||||||
|
- backup
|
||||||
|
- database
|
||||||
|
- restore
|
||||||
|
home: https://github.com/Portabase/portabase
|
||||||
|
|
||||||
|
sources:
|
||||||
|
- https://github.com/Portabase/portabase
|
||||||
|
- https://github.com/Portabase/portabase/tree/main/helm
|
||||||
|
|
||||||
|
maintainers:
|
||||||
|
- name: Charles Gauthereau
|
||||||
|
url: https://github.com/RambokDev
|
||||||
|
- name: Killian Larcher
|
||||||
|
url: https://github.com/KillianLarcher
|
||||||
|
|
||||||
|
icon: https://raw.githubusercontent.com/Portabase/portabase/main/.github/assets/logo.png
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Development Notes
|
||||||
|
|
||||||
|
## Check that Kubernetes is reachable locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install the local Portabase Helm chart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install portabase . \
|
||||||
|
--set project.secret=$(openssl rand -hex 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check the pods
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check the services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get svc
|
||||||
|
```
|
||||||
|
|
||||||
|
## If the service type is ClusterIP, expose it locally using port forwarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward svc/portabase 8887:80
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: portabase
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: portabase
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: portabase
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: portabase
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: {{ .Values.timezone }}
|
||||||
|
- name: PROJECT_URL
|
||||||
|
value: {{ .Values.project.url }}
|
||||||
|
- name: PROJECT_SECRET
|
||||||
|
value: {{ .Values.project.secret }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: portabase-data
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: portabase-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size }}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: portabase
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
selector:
|
||||||
|
app: portabase
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: {{ .Values.service.nodePort }}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
image:
|
||||||
|
repository: portabase/portabase
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1"
|
||||||
|
|
||||||
|
timezone: Europe/Paris
|
||||||
|
|
||||||
|
project:
|
||||||
|
url: http://localhost:8887
|
||||||
|
secret: "change_me_generate_secure_secret"
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClassName: ""
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 10Gi
|
||||||
|
mountPath: /data
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: nginx
|
||||||
|
hosts:
|
||||||
|
- host: portabase.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
|
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
partition: 0
|
||||||
+6
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.4.0",
|
"version": "1.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:drop": "drizzle-kit drop",
|
"db:drop": "drizzle-kit drop",
|
||||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts",
|
||||||
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.4.19",
|
"@better-auth/passkey": "^1.4.19",
|
||||||
@@ -49,7 +50,6 @@
|
|||||||
"@t3-oss/env-nextjs": "^0.13.10",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/express": "^5.0.6",
|
|
||||||
"@types/nodemailer": "^6.4.23",
|
"@types/nodemailer": "^6.4.23",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
@@ -65,7 +65,6 @@
|
|||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"drizzle-zod": "^0.7.1",
|
"drizzle-zod": "^0.7.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^5.2.1",
|
|
||||||
"googleapis": "^170.1.0",
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
@@ -106,6 +105,8 @@
|
|||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@react-email/render": "^2.0.4",
|
"@react-email/render": "^2.0.4",
|
||||||
|
"@release-it/bumper": "^7.0.5",
|
||||||
|
"@release-it/conventional-changelog": "^10.0.5",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.19.11",
|
"@types/node": "^22.19.11",
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
"framer-motion": "^12.34.3",
|
"framer-motion": "^12.34.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"release-it": "^19.2.4",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
|
|||||||
Generated
+1498
-343
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Usage: ./release <version>"
|
|
||||||
echo "Example: ./release 1.0.0"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$1
|
|
||||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
#if [ "$CURRENT_BRANCH" = "main" ]; then
|
|
||||||
# if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
# echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
|
|
||||||
# exit 1
|
|
||||||
# fi
|
|
||||||
#else
|
|
||||||
# if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
|
||||||
# echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
|
|
||||||
# exit 1
|
|
||||||
# fi
|
|
||||||
#fi
|
|
||||||
|
|
||||||
CLEAN_VERSION=${VERSION#v}
|
|
||||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
|
||||||
|
|
||||||
echo "Preparing release $VERSION..."
|
|
||||||
|
|
||||||
|
|
||||||
# package.json
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Updating package.json..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
|
||||||
else
|
|
||||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# pyproject.toml
|
|
||||||
if [ -f pyproject.toml ]; then
|
|
||||||
echo "Updating pyproject.toml..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cargo.toml
|
|
||||||
if [ -f Cargo.toml ]; then
|
|
||||||
echo "Updating Cargo.toml..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# CITATION.cff
|
|
||||||
if [ -f CITATION.cff ]; then
|
|
||||||
echo "Updating CITATION.cff..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
|
||||||
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
|
||||||
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add .
|
|
||||||
|
|
||||||
if ! git diff-index --quiet HEAD --; then
|
|
||||||
echo "Committing changes..."
|
|
||||||
git commit -m "chore(release): $VERSION"
|
|
||||||
else
|
|
||||||
echo "No changes to commit. Proceeding to tag..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo "Tag $VERSION already exists. Aborting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Creating tag $VERSION..."
|
|
||||||
git tag -a "$VERSION" -m "Release $VERSION"
|
|
||||||
|
|
||||||
echo "Pushing changes and tags to remote..."
|
|
||||||
git push
|
|
||||||
git push origin "$VERSION"
|
|
||||||
|
|
||||||
echo "Successfully released $VERSION!"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import router from "../src/features/api/router";
|
|
||||||
|
|
||||||
export function mountApi(app: express.Express) {
|
|
||||||
app.use("/services/v1", router);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import next from "next";
|
|
||||||
import express from "express";
|
|
||||||
import {mountApi} from "./api";
|
|
||||||
import http from "http";
|
|
||||||
|
|
||||||
const port = Number(process.env.PORT) || 8887;
|
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
|
||||||
|
|
||||||
async function start() {
|
|
||||||
const nextApp = next({
|
|
||||||
dev,
|
|
||||||
hostname: "0.0.0.0",
|
|
||||||
turbopack: dev,
|
|
||||||
port
|
|
||||||
});
|
|
||||||
|
|
||||||
const handle = nextApp.getRequestHandler();
|
|
||||||
|
|
||||||
await nextApp.prepare();
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
req.setTimeout(0);
|
|
||||||
res.setTimeout(0);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
mountApi(app);
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.path.startsWith('/services/v1')) return next();
|
|
||||||
handle(req, res).catch((err) => {
|
|
||||||
console.error('Next.js App Router error:', err);
|
|
||||||
res.status(500).send('Internal Server Error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
server.setTimeout(0);
|
|
||||||
server.headersTimeout = 0;
|
|
||||||
server.requestTimeout = 0;
|
|
||||||
server.keepAliveTimeout = 0;
|
|
||||||
|
|
||||||
server.listen(port, "0.0.0.0", () => {
|
|
||||||
console.log(`NEXT APP → http://localhost:${port}`);
|
|
||||||
console.log(`API → http://localhost:${port}/services/v1`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
import { Button, ButtonVariantsProps } from "@/components/ui/button";
|
||||||
import {ReactNode, useState} from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import {Loader2} from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
import {
|
||||||
import {cn} from "@/lib/utils";
|
Popover,
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export type ButtonWithConfirmProps = {
|
export type ButtonWithConfirmProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -45,7 +54,6 @@ export type ButtonWithConfirmProps = {
|
|||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
@@ -87,21 +95,24 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
|||||||
if (!isDisabled) setIsConfirming(true);
|
if (!isDisabled) setIsConfirming(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||||
{props.button?.main.icon}
|
{props.button?.main.icon}
|
||||||
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div onClick={(e) => {
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsConfirming(true);
|
setIsConfirming(true);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
const withTooltip =
|
||||||
|
isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -112,13 +123,13 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : triggerContent;
|
) : (
|
||||||
|
triggerContent
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>{withTooltip}</PopoverTrigger>
|
||||||
{withTooltip}
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-80"
|
className="w-80"
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
@@ -137,21 +148,47 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
variant={isLegacy ? (props.button?.confirm.variant ?? "default") : "default"}
|
variant={
|
||||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
isLegacy
|
||||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
? (props.button?.confirm.variant ?? "default")
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
size={
|
||||||
|
isLegacy ? (props.button?.confirm.size ?? "default") : "default"
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
isLegacy ? props.button?.confirm.className : "",
|
||||||
|
"w-full",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
{props.isPending && (
|
||||||
|
<Loader2 className="animate-spin mr-4" size={16} />
|
||||||
|
)}
|
||||||
{isLegacy && props.button?.confirm.icon}
|
{isLegacy && props.button?.confirm.icon}
|
||||||
<span>{isLegacy ? props.button?.confirm.text : (props.confirmButtonText ?? "Confirm")}</span>
|
<span>
|
||||||
|
{isLegacy
|
||||||
|
? props.button?.confirm.text
|
||||||
|
: (props.confirmButtonText ?? "Confirm")}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={isLegacy ? (props.button?.cancel.variant ?? "outline") : "outline"}
|
variant={
|
||||||
|
isLegacy
|
||||||
|
? (props.button?.cancel.variant ?? "outline")
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
className={cn(
|
||||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
isLegacy ? props.button?.cancel.className : "",
|
||||||
|
"w-full",
|
||||||
|
)}
|
||||||
|
size={
|
||||||
|
isLegacy ? (props.button?.cancel.size ?? "default") : "default"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isLegacy ? (props.button?.cancel.text ?? "Cancel") : (props.cancelButtonText ?? "Cancel")}
|
{isLegacy
|
||||||
|
? (props.button?.cancel.text ?? "Cancel")
|
||||||
|
: (props.cancelButtonText ?? "Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+55
-12
@@ -1,26 +1,69 @@
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
import {SlackChannelConfigSchema} from "./providers/notifications/forms/slack.schema";
|
||||||
|
import {SmtpChannelConfigSchema} from "./providers/notifications/forms/smtp.schema";
|
||||||
|
import {DiscordChannelConfigSchema} from "./providers/notifications/forms/discord.schema";
|
||||||
|
import {TelegramChannelConfigSchema} from "./providers/notifications/forms/telegram.schema";
|
||||||
|
import {GotifyChannelConfigSchema} from "./providers/notifications/forms/gotify.schema";
|
||||||
|
import {NtfyChannelConfigSchema} from "./providers/notifications/forms/ntfy.schema";
|
||||||
|
import {WebhookChannelConfigSchema} from "./providers/notifications/forms/webhook.schema";
|
||||||
|
import {S3ChannelConfigSchema} from "./providers/storages/forms/s3.schema";
|
||||||
|
import {GoogleDriveChannelConfigSchema} from "./providers/storages/forms/google-drive.schema";
|
||||||
|
import {LocalChannelConfigSchema} from "./providers/storages/forms/local.schema";
|
||||||
|
|
||||||
|
|
||||||
export const ChannelFormSchema = z.object({
|
const BaseChannelFormSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "Name must be at least 5 characters long")
|
.min(5, "Name must be at least 5 characters long")
|
||||||
.max(40, "Name must be at most 40 characters long"),
|
.max(40, "Name must be at most 40 characters long"),
|
||||||
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("slack"),
|
||||||
|
config: SlackChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("smtp"),
|
||||||
|
config: SmtpChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("discord"),
|
||||||
|
config: DiscordChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("telegram"),
|
||||||
|
config: TelegramChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("gotify"),
|
||||||
|
config: GotifyChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("ntfy"),
|
||||||
|
config: NtfyChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("webhook"),
|
||||||
|
config: WebhookChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
export const StorageChannelFormSchema = z.discriminatedUnion("provider", [
|
||||||
provider: z.enum(
|
BaseChannelFormSchema.extend({
|
||||||
["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"],
|
provider: z.literal("s3"),
|
||||||
{required_error: "Provider is required"}
|
config: S3ChannelConfigSchema,
|
||||||
),
|
}),
|
||||||
});
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("google-drive"),
|
||||||
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
config: GoogleDriveChannelConfigSchema,
|
||||||
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
|
}),
|
||||||
});
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("local"),
|
||||||
|
config: LocalChannelConfigSchema
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
||||||
|
|||||||
+22
-14
@@ -1,30 +1,38 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierDiscordFormProps = {
|
type NotifierDiscordFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierDiscordForm = ({form}: NotifierDiscordFormProps) => {
|
export const NotifierDiscordForm = ({ form }: NotifierDiscordFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.discordWebhook"
|
name="config.discordWebhook"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Discord Webhook URL</FormLabel>
|
<FormLabel>Discord Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://discord.com/api/webhooks/..."/>
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
placeholder="https://discord.com/api/webhooks/..."
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const DiscordChannelConfigSchema = z.object({
|
||||||
|
discordWebhook: z.string().url("Must be a valid URL"),
|
||||||
|
});
|
||||||
+23
-17
@@ -1,43 +1,49 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierGotifyFormProps = {
|
type NotifierGotifyFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierGotifyForm = ({form}: NotifierGotifyFormProps) => {
|
export const NotifierGotifyForm = ({ form }: NotifierGotifyFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.gotifyServerUrl"
|
name="config.gotifyServerUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Gotify Server URL</FormLabel>
|
<FormLabel>Gotify Server URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://gotify.example.com"/>
|
<Input {...field} placeholder="https://gotify.example.com" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.gotifyAppToken"
|
name="config.gotifyAppToken"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Application Token</FormLabel>
|
<FormLabel>Application Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const GotifyChannelConfigSchema = z.object({
|
||||||
|
gotifyServerUrl: z.string().url("Must be a valid URL"),
|
||||||
|
gotifyAppToken: z.string().min(1, "App token is required"),
|
||||||
|
});
|
||||||
+3
-2
@@ -2,6 +2,7 @@ import {UseFormReturn} from "react-hook-form";
|
|||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierNtfyFormProps = {
|
type NotifierNtfyFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>
|
||||||
@@ -48,7 +49,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Access Token (Optional)</FormLabel>
|
<FormLabel>Access Token (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="tk_..."/>
|
<PasswordInput {...field} placeholder="tk_..."/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
@@ -79,7 +80,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="password"/>
|
<PasswordInput placeholder="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const NtfyChannelConfigSchema = z.object({
|
||||||
|
ntfyTopic: z.string().min(1, "Topic is required"),
|
||||||
|
ntfyServerUrl: z.string().url("Must be a valid URL").optional().or(z.literal('')),
|
||||||
|
ntfyToken: z.string().optional(),
|
||||||
|
ntfyUsername: z.string().optional(),
|
||||||
|
ntfyPassword: z.string().optional(),
|
||||||
|
});
|
||||||
+23
-16
@@ -1,31 +1,38 @@
|
|||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {UseFormReturn} from "react-hook-form";
|
import {
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
FormControl,
|
||||||
import {Input} from "@/components/ui/input";
|
FormField,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierSmtpFormProps = {
|
type NotifierSmtpFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierSlackForm = ({form}: NotifierSmtpFormProps) => {
|
export const NotifierSlackForm = ({ form }: NotifierSmtpFormProps) => {
|
||||||
return(
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.slackWebhook"
|
name="config.slackWebhook"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Slack Webhook URL</FormLabel>
|
<FormLabel>Slack Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://hooks.slack.com/services/..."/>
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
placeholder="https://hooks.slack.com/services/..."
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const SlackChannelConfigSchema = z.object({
|
||||||
|
slackWebhook: z.string().url("Must be a valid URL"),
|
||||||
|
});
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const SmtpChannelConfigSchema = z.object({
|
||||||
|
host: z.string().min(1, "Host is required"),
|
||||||
|
port: z.coerce.number().min(1, "Port is required"),
|
||||||
|
user: z.string().min(1, "User is required"),
|
||||||
|
password: z.string().min(1, "Password is required"),
|
||||||
|
from: z.string().min(1, "From is required"),
|
||||||
|
to: z.string().min(1, "To is required"),
|
||||||
|
});
|
||||||
+56
-20
@@ -1,48 +1,84 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierTelegramFormProps = {
|
type NotifierTelegramFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
|
export const NotifierTelegramForm = ({ form }: NotifierTelegramFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.telegramBotToken"
|
name="config.telegramBotToken"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Telegram Bot Token</FormLabel>
|
<FormLabel>Telegram Bot Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
|
<PasswordInput
|
||||||
|
{...field}
|
||||||
|
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.telegramChatId"
|
name="config.telegramChatId"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Telegram Chat ID</FormLabel>
|
<FormLabel>Telegram Chat ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="-100123456789 or 123456789"/>
|
<Input {...field} placeholder="-100123456789 or 123456789" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
You must start the conversation with the bot first (<strong>/start</strong>). <br/>
|
You must start the conversation with the bot first (
|
||||||
For groups, add the bot to the group. <br/>
|
<strong>/start</strong>). <br />
|
||||||
You can use <a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer" className="underline">@userinfobot</a> to find your ID.
|
For groups, add the bot to the group. <br />
|
||||||
|
You can use{" "}
|
||||||
|
<a
|
||||||
|
href="https://t.me/userinfobot"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
@userinfobot
|
||||||
|
</a>{" "}
|
||||||
|
to find your ID.
|
||||||
</p>
|
</p>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.telegramTopicId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Topic ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="123456" />
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Unique identifier for the target message thread (topic) of the
|
||||||
|
forum; for forum supergroups only
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const TelegramChannelConfigSchema = z
|
||||||
|
.object({
|
||||||
|
telegramBotToken: z.string().min(1, "Bot token is required"),
|
||||||
|
telegramChatId: z.string().min(1, "Chat ID is required"),
|
||||||
|
telegramTopicId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.telegramTopicId && !data.telegramChatId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Chat ID is required when a Topic ID is provided",
|
||||||
|
path: ["telegramChatId"],
|
||||||
|
},
|
||||||
|
);
|
||||||
+28
-21
@@ -1,27 +1,33 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierWebhookFormProps = {
|
type NotifierWebhookFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
export const NotifierWebhookForm = ({ form }: NotifierWebhookFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookUrl"
|
name="config.webhookUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://example.com/api/webhook"/>
|
<Input {...field} placeholder="https://example.com/api/webhook" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -30,13 +36,13 @@ export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookSecretHeader"
|
name="config.webhookSecretHeader"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Header Name (Optional)</FormLabel>
|
<FormLabel>Header Name (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="X-Webhook-Secret"/>
|
<Input {...field} placeholder="X-Webhook-Secret" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -45,21 +51,22 @@ export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookSecret"
|
name="config.webhookSecret"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Value (Optional)</FormLabel>
|
<FormLabel>Secret Value (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="Secret value..."/>
|
<PasswordInput {...field} placeholder="Secret value..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
If provided, the secret will be sent in the specified header (defaults to <code>X-Webhook-Secret</code>).
|
If provided, the secret will be sent in the specified header (defaults
|
||||||
|
to <code>X-Webhook-Secret</code>).
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const WebhookChannelConfigSchema = z.object({
|
||||||
|
webhookUrl: z.string().url("Must be a valid URL"),
|
||||||
|
webhookSecretHeader: z.string().optional(),
|
||||||
|
webhookSecret: z.string().optional(),
|
||||||
|
});
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const GoogleDriveChannelConfigSchema = z.object({
|
||||||
|
clientId: z.string().min(1, "Client ID is required"),
|
||||||
|
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||||
|
folderId: z.string().min(1, "Folder ID is required"),
|
||||||
|
refreshToken: z.string().optional(),
|
||||||
|
});
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const LocalChannelConfigSchema = z.object({});
|
||||||
+34
-33
@@ -1,81 +1,86 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
FormItem,
|
||||||
import {Switch} from "@/components/ui/switch";
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
type StorageS3FormProps = {
|
type StorageS3FormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
export const StorageS3Form = ({ form }: StorageS3FormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.endPointUrl"
|
name="config.endPointUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Endpoint URL</FormLabel>
|
<FormLabel>Endpoint URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="s3.amazonaws.com"/>
|
<Input {...field} placeholder="s3.amazonaws.com" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.region"
|
name="config.region"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Region</FormLabel>
|
<FormLabel>Region</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="us-east-1"/>
|
<Input {...field} placeholder="us-east-1" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.accessKey"
|
name="config.accessKey"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Access Key</FormLabel>
|
<FormLabel>Access Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<Input {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.secretKey"
|
name="config.secretKey"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Key</FormLabel>
|
<FormLabel>Secret Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.bucketName"
|
name="config.bucketName"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Bucket name</FormLabel>
|
<FormLabel>Bucket name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="portabase-dev"/>
|
<Input {...field} placeholder="portabase-dev" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -100,16 +105,12 @@ export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Use SSL</FormLabel>
|
<FormLabel>Use SSL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const S3ChannelConfigSchema = z.object({
|
||||||
|
endPointUrl: z.string().min(1, "Endpoint URL is required"),
|
||||||
|
region: z.string().optional(),
|
||||||
|
accessKey: z.string().min(1, "Access Key is required"),
|
||||||
|
secretKey: z.string().min(1, "Secret Key is required"),
|
||||||
|
bucketName: z.string().min(1, "Bucket name is required"),
|
||||||
|
port: z.coerce.number().optional(),
|
||||||
|
ssl: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
@@ -1,38 +1,43 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import { EmptyStatePlaceholder } from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import { OrganizationWithMembers } from "@/db/schema/03_organization";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import { StorageChannelWith } from "@/db/schema/12_storage-channel";
|
||||||
import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
import { ChannelCard } from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
||||||
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
import { ChannelAddEditModal } from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
import {
|
||||||
|
ChannelKind,
|
||||||
|
getChannelTextBasedOnKind,
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
type ChannelsSectionProps = {
|
type ChannelsSectionProps = {
|
||||||
channels: NotificationChannelWith[] | StorageChannelWith[],
|
channels: NotificationChannelWith[] | StorageChannelWith[];
|
||||||
organizations: OrganizationWithMembers[],
|
organizations: OrganizationWithMembers[];
|
||||||
kind: ChannelKind,
|
kind: ChannelKind;
|
||||||
defaultStorageChannelId?: string | null | undefined
|
defaultStorageChannelId?: string | null | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ChannelsSection = ({
|
export const ChannelsSection = ({
|
||||||
organizations,
|
organizations,
|
||||||
channels,
|
channels,
|
||||||
kind,
|
kind,
|
||||||
defaultStorageChannelId
|
defaultStorageChannelId,
|
||||||
}: ChannelsSectionProps) => {
|
}: ChannelsSectionProps) => {
|
||||||
|
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const channelText = getChannelTextBasedOnKind(kind)
|
const channelText = getChannelTextBasedOnKind(kind);
|
||||||
const hasChannels = channels.length > 0
|
const hasChannels = channels.length > 0;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<ChannelAddEditModal kind={kind} open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen}
|
<ChannelAddEditModal
|
||||||
|
kind={kind}
|
||||||
|
open={isAddModalOpen}
|
||||||
|
onOpenChangeAction={setIsAddModalOpen}
|
||||||
adminView={false}
|
adminView={false}
|
||||||
trigger={false}/>
|
trigger={false}
|
||||||
|
/>
|
||||||
{hasChannels ? (
|
{hasChannels ? (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
@@ -50,11 +55,11 @@ export const ChannelsSection = ({
|
|||||||
<EmptyStatePlaceholder
|
<EmptyStatePlaceholder
|
||||||
text={`No ${channelText} channels configured yet`}
|
text={`No ${channelText} channels configured yet`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsAddModalOpen(true)
|
setIsAddModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="h-full"
|
className="h-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+211
-32
@@ -1,62 +1,241 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {Eye} from "lucide-react";
|
import {
|
||||||
|
AlertCircle,
|
||||||
import {useState} from "react";
|
Braces,
|
||||||
|
Database,
|
||||||
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
Hash,
|
||||||
|
Server,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {Button} from "@/components/ui/button";
|
import type { NotificationLogWithRelations } from "@/db/services/notification-log";
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import {NotificationLogWithRelations} from "@/db/services/notification-log";
|
|
||||||
|
|
||||||
type NotificationLogModalProps = {
|
type NotificationLogModalProps = {
|
||||||
notificationLog: NotificationLogWithRelations;
|
notificationLog: NotificationLogWithRelations;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotificationLogModal = ({notificationLog}: NotificationLogModalProps) => {
|
const getTroubleshootingForError = (errorMsg?: any) => {
|
||||||
|
if (!errorMsg || String(errorMsg).toLowerCase() === "null") return null;
|
||||||
|
|
||||||
|
const msg = String(errorMsg).toLowerCase();
|
||||||
|
|
||||||
|
if (msg.includes("database connection") || msg.includes("agent")) {
|
||||||
|
return {
|
||||||
|
title: "Agent Connection Issue",
|
||||||
|
resolution:
|
||||||
|
"It appears the agent cannot reach the database. Please ensure that your agent is actively running, your PostgreSQL credentials are correct, and port 5432 is open on your firewall.",
|
||||||
|
docLink:
|
||||||
|
"https://portabase.io/docs/agent/troubleshooting/agent-connection",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.includes("timeout")) {
|
||||||
|
return {
|
||||||
|
title: "Timeout",
|
||||||
|
resolution:
|
||||||
|
"The agent is taking too long to respond. This could be due to high server load, network issues, or a large database.",
|
||||||
|
docLink: "https://portabase.io/docs/agent/troubleshooting/timeout",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: "Unexpected Execution Error",
|
||||||
|
resolution:
|
||||||
|
"An unexpected error occurred during execution. Please review the raw system logs or contact support if the issue persists.",
|
||||||
|
docLink: "https://discord.gg/Wgv7xZ8fWJ",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconForKey = (key: string, value: any) => {
|
||||||
|
const isActualError =
|
||||||
|
key.toLowerCase().includes("error") &&
|
||||||
|
value !== null &&
|
||||||
|
String(value).toLowerCase() !== "null";
|
||||||
|
|
||||||
|
if (key.toLowerCase().includes("id"))
|
||||||
|
return <Hash className="w-3.5 h-3.5 text-muted-foreground" />;
|
||||||
|
if (key.toLowerCase().includes("host"))
|
||||||
|
return <Server className="w-3.5 h-3.5 text-muted-foreground" />;
|
||||||
|
if (isActualError)
|
||||||
|
return <AlertCircle className="w-3.5 h-3.5 text-destructive" />;
|
||||||
|
return <Database className="w-3.5 h-3.5 text-muted-foreground" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationLogModal = ({
|
||||||
|
notificationLog,
|
||||||
|
}: NotificationLogModalProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// TODO : to be fixed
|
||||||
|
// @ts-ignore
|
||||||
|
const payloadError = notificationLog.payload?.error;
|
||||||
|
const troubleshooting = getTroubleshootingForError(payloadError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
<Button
|
||||||
<Eye/>
|
variant="outline"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-xl bg-background">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Notification details</DialogTitle>
|
<DialogTitle>Notification Details</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Execution logs and payload data</DialogDescription>
|
||||||
Details of the notification sent
|
</DialogHeader>
|
||||||
</DialogDescription>
|
|
||||||
<Separator className="mt-3 mb-3"/>
|
|
||||||
|
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-4 mt-2 max-h-[70vh] overflow-y-auto px-1 pb-2">
|
||||||
<div>
|
<div className="relative border rounded-xl bg-card shadow-sm flex flex-col">
|
||||||
<span className="font-semibold">Title:</span>{" "}
|
<div className="bg-primary/5 border-b px-4 py-2.5 flex items-center gap-2 rounded-t-xl">
|
||||||
<span>{notificationLog.content.title}</span>
|
<div className="p-1 bg-primary/10 rounded-md">
|
||||||
|
<Database className="w-4 h-4 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<span className="font-semibold text-sm text-primary">
|
||||||
<span className="font-semibold">Message:</span>{" "}
|
Event Trigger
|
||||||
<span>{notificationLog.content.message}</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-1 sm:gap-4">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground min-w-20 pt-0.5">
|
||||||
|
Title
|
||||||
|
</span>
|
||||||
|
<span className="text-sm sm:text-right flex-1 wrap-break-words">
|
||||||
|
{notificationLog.content.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-1 sm:gap-4">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground min-w-20 pt-0.5">
|
||||||
|
Message
|
||||||
|
</span>
|
||||||
|
<span className="text-sm sm:text-right flex-1 wrap-break-word text-muted-foreground">
|
||||||
|
{notificationLog.content.message}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notificationLog.payload && (
|
{notificationLog.payload && (
|
||||||
|
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-primary rounded-full border-2 border-background z-10" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-border mx-auto -my-4 relative z-0" />
|
||||||
|
|
||||||
|
{notificationLog.payload &&
|
||||||
|
Object.keys(notificationLog.payload).length > 0 && (
|
||||||
|
<div className="relative border rounded-xl bg-card shadow-sm flex flex-col">
|
||||||
|
<div className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-primary rounded-full border-2 border-background z-10" />
|
||||||
|
|
||||||
|
<div className="bg-primary/5 border-b px-4 py-2.5 flex items-center justify-between rounded-t-xl">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1 bg-primary/10 rounded-md">
|
||||||
|
<Braces className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold text-sm text-primary">
|
||||||
|
JSON Payload
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{Object.entries(notificationLog.payload).map(
|
||||||
|
([key, value], index, arr) => {
|
||||||
|
const isActualError =
|
||||||
|
key === "error" &&
|
||||||
|
value !== null &&
|
||||||
|
String(value).toLowerCase() !== "null";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className={`flex flex-col sm:flex-row sm:items-center justify-between gap-2 px-4 py-3 hover:bg-muted/50 transition-colors ${
|
||||||
|
index !== arr.length - 1
|
||||||
|
? "border-b border-border/50"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-[150px]">
|
||||||
|
{getIconForKey(key, value)}
|
||||||
|
<span className="text-sm font-medium">{key}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 sm:text-right">
|
||||||
|
{isActualError ? (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-md bg-destructive/10 text-destructive text-xs font-mono break-all">
|
||||||
|
{String(value)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground font-mono bg-muted px-2 py-1 rounded-md break-all">
|
||||||
|
{String(value)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{troubleshooting && (
|
||||||
|
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-primary rounded-full border-2 border-background z-10" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{troubleshooting && (
|
||||||
<>
|
<>
|
||||||
<Separator className="mb-3" />
|
<div className="w-px h-6 bg-border mx-auto -my-4 relative z-0" />
|
||||||
<div className="text-sm font-mono bg-gray-100 dark:bg-gray-800 dark:text-gray-200 p-4 rounded-md overflow-auto">
|
|
||||||
{JSON.stringify(notificationLog.payload, null, 2)}
|
<div className="relative border border-destructive/20 rounded-xl bg-gradient-to-b from-destructive/5 to-transparent shadow-sm flex flex-col">
|
||||||
|
<div className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-destructive rounded-full border-2 border-background z-10" />
|
||||||
|
|
||||||
|
<div className="bg-destructive/10 border-b border-destructive/10 px-4 py-2.5 flex items-center justify-between rounded-t-xl">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1 bg-destructive/20 rounded-md">
|
||||||
|
<Sparkles className="w-4 h-4 text-destructive" />
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold text-sm text-destructive">
|
||||||
|
Suggested Resolution
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 flex flex-col gap-2">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground">
|
||||||
|
{troubleshooting.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{troubleshooting.resolution}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<a
|
||||||
|
href={troubleshooting.docLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 text-xs font-semibold text-destructive hover:text-destructive/80 transition-colors"
|
||||||
|
>
|
||||||
|
Read the documentation{" "}
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogHeader>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {backupButtonAction} from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
|
import { backupButtonAction } from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import { Check, DatabaseZap, X } from "lucide-react";
|
||||||
import {Database, DatabaseZap} from "lucide-react";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
|
|
||||||
export type BackupButtonProps = {
|
export type BackupButtonProps = {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
@@ -17,14 +17,18 @@ export type BackupButtonProps = {
|
|||||||
export const BackupButton = (props: BackupButtonProps) => {
|
export const BackupButton = (props: BackupButtonProps) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (databaseId: string) => {
|
mutationFn: async (databaseId: string) => {
|
||||||
const backup = await backupButtonAction(databaseId);
|
const backup = await backupButtonAction(databaseId);
|
||||||
if (backup?.data?.success) {
|
if (backup?.data?.success) {
|
||||||
toast.success(backup.data.actionSuccess?.message || "Backup created successfully!");
|
toast.success(
|
||||||
queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]});
|
backup.data.actionSuccess?.message || "Backup created successfully!",
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["database-data", props.databaseId],
|
||||||
|
});
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
toast.error(backup?.serverError || "Failed to create backup.");
|
toast.error(backup?.serverError || "Failed to create backup.");
|
||||||
@@ -36,14 +40,33 @@ export const BackupButton = (props: BackupButtonProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonWithLoading
|
<ButtonWithConfirm
|
||||||
icon={<DatabaseZap/>}
|
title="Create Backup"
|
||||||
disabled={props.disable}
|
description={"Are you sure you want to create a backup?"}
|
||||||
isPending={mutation.isPending}
|
button={{
|
||||||
size={"default"}
|
main: {
|
||||||
onClick={async () => {
|
disabled: props.disable,
|
||||||
|
text: isMobile ? "" : "Backup",
|
||||||
|
variant: "default",
|
||||||
|
icon: <DatabaseZap />,
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Yes, create backup",
|
||||||
|
icon: <Check />,
|
||||||
|
variant: "default",
|
||||||
|
onClick: async () => {
|
||||||
await HandleAction();
|
await HandleAction();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "No, cancel",
|
||||||
|
icon: <X />,
|
||||||
|
variant: "outline",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>{isMobile ? "" : "Backup"}</ButtonWithLoading>
|
isPending={mutation.isPending}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
import { GearIcon } from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
|
|
||||||
export type EditButtonProps = {};
|
|
||||||
|
|
||||||
export const EditButton = (props: EditButtonProps) => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link className={buttonVariants({ variant: "outline" })} href={`${pathname}/edit`}>
|
|
||||||
<GearIcon className="w-7 h-7" />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,47 +1,50 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {DatabaseBackupActionsModal} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-modal";
|
import { DatabaseBackupActionsModal } from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-modal";
|
||||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
import { DatabaseTabs } from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import { Setting } from "@/db/schema/01_setting";
|
||||||
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database";
|
import { BackupWith, DatabaseWith, Restoration } from "@/db/schema/07_database";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||||
import {useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
import { useBackupModal } from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||||
import {useQuery, useQueryClient} from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {getDatabaseDataAction} from "@/components/wrappers/dashboard/database/backup/actions/get-data.action";
|
import { getDatabaseDataAction } from "@/components/wrappers/dashboard/database/backup/actions/get-data.action";
|
||||||
import {PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
PageContent,
|
||||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
PageDescription,
|
||||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
PageTitle,
|
||||||
import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
|
} from "@/features/layout/page";
|
||||||
import {HardDrive, Megaphone} from "lucide-react";
|
import { capitalizeFirstLetter } from "@/utils/text";
|
||||||
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
import { RetentionPolicySheet } from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
import { CronButton } from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||||
|
import { ChannelPoliciesModal } from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
|
||||||
|
import { HardDrive, Megaphone } from "lucide-react";
|
||||||
|
import { ImportModal } from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||||
|
import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
|
|
||||||
export type DatabaseContentProps = {
|
export type DatabaseContentProps = {
|
||||||
settings: Setting,
|
settings: Setting;
|
||||||
backups: BackupWith[],
|
backups: BackupWith[];
|
||||||
restorations: Restoration[],
|
restorations: Restoration[];
|
||||||
isAlreadyRestore: boolean,
|
isAlreadyRestore: boolean;
|
||||||
database: DatabaseWith,
|
database: DatabaseWith;
|
||||||
activeMember: MemberWithUser,
|
activeMember: MemberWithUser;
|
||||||
totalBackups: number,
|
totalBackups: number;
|
||||||
availableBackups: number,
|
availableBackups: number;
|
||||||
successRate: number | null,
|
successRate: number | null;
|
||||||
organizationId: string,
|
organizationId: string;
|
||||||
activeOrganizationChannels: any[],
|
activeOrganizationChannels: any[];
|
||||||
activeOrganizationStorageChannels: any[]
|
activeOrganizationStorageChannels: any[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export const DatabaseContent = (props: DatabaseContentProps) => {
|
export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||||
const {} = useBackupModal();
|
const {} = useBackupModal();
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
const {data} = useQuery({
|
|
||||||
queryKey: ["database-data", props.database.id],
|
queryKey: ["database-data", props.database.id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await getDatabaseDataAction({databaseId: props.database.id});
|
const result = await getDatabaseDataAction({
|
||||||
|
databaseId: props.database.id,
|
||||||
|
});
|
||||||
return result?.data;
|
return result?.data;
|
||||||
},
|
},
|
||||||
initialData: {
|
initialData: {
|
||||||
@@ -54,12 +57,13 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
backups: props.backups,
|
backups: props.backups,
|
||||||
restorations: props.restorations,
|
restorations: props.restorations,
|
||||||
activeOrganizationChannels: props.activeOrganizationChannels,
|
activeOrganizationChannels: props.activeOrganizationChannels,
|
||||||
activeOrganizationStorageChannels: props.activeOrganizationStorageChannels,
|
activeOrganizationStorageChannels:
|
||||||
|
props.activeOrganizationStorageChannels,
|
||||||
stats: {
|
stats: {
|
||||||
totalBackups: props.totalBackups,
|
totalBackups: props.totalBackups,
|
||||||
availableBackups: props.availableBackups,
|
availableBackups: props.availableBackups,
|
||||||
successRate: props.successRate
|
successRate: props.successRate,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
@@ -69,16 +73,21 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
const database = data?.database ?? props.database;
|
const database = data?.database ?? props.database;
|
||||||
const backups = data?.backups ?? props.backups;
|
const backups = data?.backups ?? props.backups;
|
||||||
const restorations = data?.restorations ?? props.restorations;
|
const restorations = data?.restorations ?? props.restorations;
|
||||||
const activeOrganizationChannels = data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
|
const activeOrganizationChannels =
|
||||||
const activeOrganizationStorageChannels = data?.activeOrganizationStorageChannels ?? props.activeOrganizationStorageChannels;
|
data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
|
||||||
|
const activeOrganizationStorageChannels =
|
||||||
|
data?.activeOrganizationStorageChannels ??
|
||||||
|
props.activeOrganizationStorageChannels;
|
||||||
const stats = data?.stats ?? {
|
const stats = data?.stats ?? {
|
||||||
totalBackups: props.totalBackups,
|
totalBackups: props.totalBackups,
|
||||||
availableBackups: props.availableBackups,
|
availableBackups: props.availableBackups,
|
||||||
successRate: props.successRate
|
successRate: props.successRate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
const isAlreadyBackup = backups.some(
|
||||||
|
(b) => b.status === "waiting" || b.status === "ongoing",
|
||||||
|
);
|
||||||
|
|
||||||
const isMember = props.activeMember.role === "member";
|
const isMember = props.activeMember.role === "member";
|
||||||
|
|
||||||
@@ -92,26 +101,29 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
{!isMember && (
|
{!isMember && (
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RetentionPolicySheet database={database}/>
|
<RetentionPolicySheet database={database} />
|
||||||
<CronButton database={database}/>
|
<CronButton database={database} />
|
||||||
<ChannelPoliciesModal
|
<ChannelPoliciesModal
|
||||||
database={database}
|
database={database}
|
||||||
kind={"notification"}
|
kind={"notification"}
|
||||||
icon={<Megaphone/>}
|
icon={<Megaphone />}
|
||||||
channels={activeOrganizationChannels}
|
channels={activeOrganizationChannels}
|
||||||
organizationId={props.organizationId}
|
organizationId={props.organizationId}
|
||||||
/>
|
/>
|
||||||
<ChannelPoliciesModal
|
<ChannelPoliciesModal
|
||||||
database={database}
|
database={database}
|
||||||
icon={<HardDrive/>}
|
icon={<HardDrive />}
|
||||||
kind={"storage"}
|
kind={"storage"}
|
||||||
channels={activeOrganizationStorageChannels}
|
channels={activeOrganizationStorageChannels}
|
||||||
organizationId={props.organizationId}
|
organizationId={props.organizationId}
|
||||||
/>
|
/>
|
||||||
<ImportModal database={database}/>
|
<ImportModal database={database} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={database.id}/>
|
<BackupButton
|
||||||
|
disable={isAlreadyBackup}
|
||||||
|
databaseId={database.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -119,7 +131,9 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{database.description && (
|
{database.description && (
|
||||||
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
|
<PageDescription className="mt-5 sm:mt-0">
|
||||||
|
{database.description}
|
||||||
|
</PageDescription>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
@@ -129,7 +143,7 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
availableBackups={stats.availableBackups}
|
availableBackups={stats.availableBackups}
|
||||||
totalBackups={stats.totalBackups}
|
totalBackups={stats.totalBackups}
|
||||||
/>
|
/>
|
||||||
<DatabaseBackupActionsModal/>
|
<DatabaseBackupActionsModal />
|
||||||
<DatabaseTabs
|
<DatabaseTabs
|
||||||
activeMember={props.activeMember}
|
activeMember={props.activeMember}
|
||||||
settings={props.settings}
|
settings={props.settings}
|
||||||
@@ -140,5 +154,5 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
|||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+2
-2
@@ -69,8 +69,8 @@ export const env = createEnv({
|
|||||||
AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"),
|
AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"),
|
||||||
AUTH_ROLE_MAP: z.string().optional(),
|
AUTH_ROLE_MAP: z.string().optional(),
|
||||||
AUTH_DEFAULT_ROLE: z.string().optional().default("pending"),
|
AUTH_DEFAULT_ROLE: z.string().optional().default("pending"),
|
||||||
AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("false"),
|
AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("true"),
|
||||||
AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("false"),
|
AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("true"),
|
||||||
|
|
||||||
PRIVATE_PATH: z.string().optional(),
|
PRIVATE_PATH: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,29 +7,29 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||||
import {Restoration} from "@/db/schema/07_database";
|
import { Restoration } from "@/db/schema/07_database";
|
||||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
import { formatLocalizedDate } from "@/utils/date-formatting";
|
||||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
deleteRestoreAction,
|
deleteRestoreAction,
|
||||||
rerunRestorationAction
|
rerunRestorationAction,
|
||||||
} from "@/features/dashboard/restore/restore.action";
|
} from "@/features/dashboard/restore/restore.action";
|
||||||
import {toast} from "sonner";
|
import { toast } from "sonner";
|
||||||
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
|
import { TooltipCustom } from "@/components/wrappers/common/tooltip-custom";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||||
|
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
|
|
||||||
export function restoreColumns(
|
export function restoreColumns(
|
||||||
isAlreadyRestore: boolean,
|
isAlreadyRestore: boolean,
|
||||||
activeMember: MemberWithUser
|
activeMember: MemberWithUser,
|
||||||
): ColumnDef<Restoration>[] {
|
): ColumnDef<Restoration>[] {
|
||||||
return[
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: "Reference",
|
header: "Reference",
|
||||||
@@ -38,7 +38,7 @@ export function restoreColumns(
|
|||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: "Created At",
|
header: "Created At",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatLocalizedDate(row.getValue("createdAt"))
|
return formatLocalizedDate(row.getValue("createdAt"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,7 +56,6 @@ export function restoreColumns(
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const rowData: Restoration = row.original;
|
const rowData: Restoration = row.original;
|
||||||
|
|
||||||
|
|
||||||
const mutationDeleteRestore = useMutation({
|
const mutationDeleteRestore = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const restoration = await deleteRestoreAction({
|
const restoration = await deleteRestoreAction({
|
||||||
@@ -66,7 +65,9 @@ export function restoreColumns(
|
|||||||
if (restoration.data.success) {
|
if (restoration.data.success) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
toast.success(restoration.data.actionSuccess.message);
|
toast.success(restoration.data.actionSuccess.message);
|
||||||
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["database-data", rowData.databaseId],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
toast.error(restoration.data.actionError.message);
|
toast.error(restoration.data.actionError.message);
|
||||||
@@ -83,7 +84,9 @@ export function restoreColumns(
|
|||||||
if (restoration.data.success) {
|
if (restoration.data.success) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
toast.success(restoration.data.actionSuccess.message);
|
toast.success(restoration.data.actionSuccess.message);
|
||||||
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["database-data", rowData.databaseId],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
toast.error(restoration.data.actionError.message);
|
toast.error(restoration.data.actionError.message);
|
||||||
@@ -91,51 +94,84 @@ export function restoreColumns(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await mutationDeleteRestore.mutateAsync();
|
await mutationDeleteRestore.mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRerunRestore = async () => {
|
const handleRerunRestore = async () => {
|
||||||
await mutationRerunRestore.mutateAsync();
|
await mutationRerunRestore.mutateAsync();
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{activeMember.role != "member" && (
|
{activeMember.role !== "member" && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0" type="button" onClick={(e) => e.stopPropagation()}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4"/>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
{rowData.backupStorageId && (
|
{rowData.backupStorageId && (
|
||||||
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting">
|
<TooltipCustom
|
||||||
|
disabled={isAlreadyRestore}
|
||||||
|
text="Already a restoration waiting"
|
||||||
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={mutationRerunRestore.isPending || isAlreadyRestore}
|
disabled={
|
||||||
|
mutationRerunRestore.isPending || isAlreadyRestore
|
||||||
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await handleRerunRestore();
|
await handleRerunRestore();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReloadIcon/> Rerun
|
<ReloadIcon /> Rerun
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</TooltipCustom>
|
</TooltipCustom>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator/>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem>
|
||||||
disabled={status == "waiting"}
|
<ButtonWithConfirm
|
||||||
className="text-red-600"
|
title="Delete Restoration"
|
||||||
onClick={async () => {
|
description={
|
||||||
|
"Are you sure you want to delete this restoration?"
|
||||||
|
}
|
||||||
|
button={{
|
||||||
|
main: {
|
||||||
|
text: "Delete",
|
||||||
|
variant: "ghost",
|
||||||
|
icon: (
|
||||||
|
<Trash2 className="text-red-500 size-4 mr-px" />
|
||||||
|
),
|
||||||
|
disabled: status === "waiting",
|
||||||
|
className:
|
||||||
|
"text-red-500 hover:text-red-500 p-0 h-auto has-[>svg]:px-0",
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "Yes, delete",
|
||||||
|
icon: <Check />,
|
||||||
|
variant: "destructive",
|
||||||
|
onClick: async () => {
|
||||||
await handleDelete();
|
await handleDelete();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
className: "w-full",
|
||||||
|
text: "No, cancel",
|
||||||
|
icon: <X />,
|
||||||
|
variant: "outline",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
isPending={mutationDeleteRestore.isPending}
|
||||||
<Trash2/> Delete
|
/>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -144,6 +180,5 @@ export function restoreColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
import type {EventPayload, DispatchResult} from '../types';
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
const getPriority = (level?: string): number => {
|
||||||
|
switch (level) {
|
||||||
|
case 'critical': return 5;
|
||||||
|
case 'error': return 4;
|
||||||
|
case 'warning': return 3;
|
||||||
|
case 'info':
|
||||||
|
default: return 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatData = (data: any): string => {
|
||||||
|
if (!data || typeof data !== 'object') return '';
|
||||||
|
return '\n\nDetails:\n' + Object.entries(data)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const stringVal = value !== null && typeof value === 'object' ? JSON.stringify(value) : String(value);
|
||||||
|
return `- ${key}: ${stringVal}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTags = (level?: string): string[] => {
|
||||||
|
const baseTags = ['floppy_disk'];
|
||||||
|
|
||||||
|
if (level === 'critical') baseTags.push('rotating_light');
|
||||||
|
else if (level === 'error') baseTags.push('x');
|
||||||
|
else if (level === 'warning') baseTags.push('warning');
|
||||||
|
else baseTags.push('information_source');
|
||||||
|
|
||||||
|
return baseTags;
|
||||||
|
};
|
||||||
|
|
||||||
export async function sendNtfy(
|
export async function sendNtfy(
|
||||||
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string, ntfyUsername?: string, ntfyPassword?: string },
|
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string, ntfyUsername?: string, ntfyPassword?: string },
|
||||||
payload: EventPayload
|
payload: EventPayload
|
||||||
@@ -11,9 +42,9 @@ export async function sendNtfy(
|
|||||||
const body = {
|
const body = {
|
||||||
topic: ntfyTopic,
|
topic: ntfyTopic,
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
message: payload.message + (payload.data ? `\n\nData:\n${JSON.stringify(payload.data, null, 2)}` : ''),
|
message: payload.message + formatData(payload.data),
|
||||||
priority: 1,
|
priority: getPriority(payload.level),
|
||||||
tags: [payload.level === 'critical' ? 'rotating_light' : payload.level === 'warning' ? 'warning' : 'information_source'],
|
tags: getTags(payload.level),
|
||||||
};
|
};
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type {EventPayload, DispatchResult} from '../types';
|
import type { EventPayload, DispatchResult } from "../types";
|
||||||
|
|
||||||
export async function sendTelegram(
|
export async function sendTelegram(
|
||||||
config: { telegramBotToken: string; telegramChatId: string },
|
config: {
|
||||||
payload: EventPayload
|
telegramBotToken: string;
|
||||||
|
telegramChatId: string;
|
||||||
|
telegramTopicId?: string;
|
||||||
|
},
|
||||||
|
payload: EventPayload,
|
||||||
): Promise<DispatchResult> {
|
): Promise<DispatchResult> {
|
||||||
const {telegramBotToken, telegramChatId} = config;
|
const { telegramBotToken, telegramChatId, telegramTopicId } = config;
|
||||||
|
|
||||||
// Helper to escape HTML characters
|
// Helper to escape HTML characters
|
||||||
const escapeHtml = (unsafe: string) => {
|
const escapeHtml = (unsafe: string) => {
|
||||||
@@ -19,21 +23,30 @@ export async function sendTelegram(
|
|||||||
const title = escapeHtml(payload.title);
|
const title = escapeHtml(payload.title);
|
||||||
const message = escapeHtml(payload.message);
|
const message = escapeHtml(payload.message);
|
||||||
const level = escapeHtml(payload.level.toUpperCase());
|
const level = escapeHtml(payload.level.toUpperCase());
|
||||||
const dataString = payload.data ? escapeHtml(JSON.stringify(payload.data, null, 2).substring(0, 1000)) : '';
|
const dataString = payload.data
|
||||||
|
? escapeHtml(JSON.stringify(payload.data, null, 2).substring(0, 1000))
|
||||||
|
: "";
|
||||||
|
|
||||||
const text = `<b>${title}</b>\n\n${message}\n\nLevel: <code>${level}</code>${payload.data ? `\n\nData:\n<pre>${dataString}</pre>` : ''}`;
|
const text = `<b>${title}</b>\n\n${message}\n\nLevel: <code>${level}</code>${payload.data ? `\n\nData:\n<pre>${dataString}</pre>` : ""}`;
|
||||||
|
|
||||||
const body = {
|
const body: Record<string, any> = {
|
||||||
chat_id: telegramChatId,
|
chat_id: telegramChatId,
|
||||||
text,
|
text,
|
||||||
parse_mode: 'HTML',
|
parse_mode: "HTML",
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
|
if (telegramTopicId) {
|
||||||
method: 'POST',
|
body.message_thread_id = Number(telegramTopicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.telegram.org/bot${telegramBotToken}/sendMessage`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.text();
|
const err = await res.text();
|
||||||
@@ -42,8 +55,8 @@ export async function sendTelegram(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: 'telegram',
|
provider: "telegram",
|
||||||
message: 'Sent to Telegram',
|
message: "Sent to Telegram",
|
||||||
response: await res.text(),
|
response: await res.text(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user