mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92e671622 | ||
|
|
2bd25353da | ||
|
|
e1bee19c18 | ||
|
|
0ec49af72a | ||
|
|
6d4eb836ab | ||
|
|
0c3cfecb1d | ||
|
|
f9186e7354 | ||
|
|
bc603a08cf | ||
|
|
077aada056 | ||
|
|
4ed5f9872d | ||
|
|
2d6be20b30 | ||
|
|
be8f3bd98c | ||
|
|
a3a20bee4a | ||
|
|
09a1e75acf | ||
|
|
ce6bdd01ef | ||
|
|
a03a1d635f | ||
|
|
419b6d4891 | ||
|
|
b4840318d5 | ||
|
|
bf81f437d1 | ||
|
|
81a4d33df1 | ||
|
|
baab364b56 | ||
|
|
585ee041ec | ||
|
|
da3d4350cc | ||
|
|
df9390847f | ||
|
|
2a1acbfde8 | ||
|
|
16ef8507b0 | ||
|
|
df5d4e336c | ||
|
|
ad2015329b | ||
|
|
4cc4bed785 | ||
|
|
6c506e6907 | ||
|
|
977f9344c7 | ||
|
|
2adb2f3b68 | ||
|
|
40a6e9e69f | ||
|
|
e1d1a3fb9a | ||
|
|
75120c0a56 | ||
|
|
33f1ef3bb3 | ||
|
|
b6e742333c | ||
|
|
9f8c66fc20 | ||
|
|
3b93257890 | ||
|
|
1104e2a074 | ||
|
|
3e950652e8 | ||
|
|
98e40466a0 | ||
|
|
324d80a735 | ||
|
|
6770be63b6 | ||
|
|
89aa33ebd8 | ||
|
|
619c32f09d | ||
|
|
c471423775 | ||
|
|
c589b9b8b8 | ||
|
|
bae9ba0973 | ||
|
|
7bfc5c1d9b | ||
|
|
8bd3b0d731 | ||
|
|
06cd4de141 | ||
|
|
33eb2df023 | ||
|
|
540ecd4b60 | ||
|
|
237418f009 |
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
We take security seriously and aim to support the following versions of the project with security updates:
|
We take security seriously and aim to support the following versions of the project with security updates:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|-----------|--------------------|
|
|---------|--------------------|
|
||||||
| 1.x | ✅ Fully Supported |
|
| Latest | ✅ Fully Supported |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,155 @@
|
|||||||
|
#name: Docker Publish
|
||||||
|
#
|
||||||
|
#on:
|
||||||
|
# workflow_call:
|
||||||
|
# inputs:
|
||||||
|
# image_name:
|
||||||
|
# required: false
|
||||||
|
# type: string
|
||||||
|
# default: 'solucetechnologies/portabase'
|
||||||
|
# add_latest:
|
||||||
|
# required: false
|
||||||
|
# type: boolean
|
||||||
|
# default: false
|
||||||
|
# target:
|
||||||
|
# required: false
|
||||||
|
# type: string
|
||||||
|
# default: 'prod'
|
||||||
|
# dockerfile:
|
||||||
|
# required: false
|
||||||
|
# type: string
|
||||||
|
# default: './docker/dockerfile/Dockerfile'
|
||||||
|
# secrets:
|
||||||
|
# DOCKER_USERNAME:
|
||||||
|
# required: true
|
||||||
|
# DOCKER_PASSWORD:
|
||||||
|
# required: true
|
||||||
|
#
|
||||||
|
#jobs:
|
||||||
|
# build-and-push:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# permissions:
|
||||||
|
# packages: write
|
||||||
|
# contents: read
|
||||||
|
# steps:
|
||||||
|
# - name: Check out the repo
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
#
|
||||||
|
# - name: Log in to Docker Hub
|
||||||
|
# uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
#
|
||||||
|
# - name: Set tags
|
||||||
|
# id: set-tags
|
||||||
|
# run: |
|
||||||
|
# REF_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
# TAGS="${{ inputs.image_name }}:$REF_NAME"
|
||||||
|
# if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
||||||
|
# TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
||||||
|
# fi
|
||||||
|
# echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
#
|
||||||
|
# - name: Build and push Docker image
|
||||||
|
# uses: docker/build-push-action@v6
|
||||||
|
# with:
|
||||||
|
# context: .
|
||||||
|
# file: ${{ inputs.dockerfile }}
|
||||||
|
# push: true
|
||||||
|
# tags: ${{ steps.set-tags.outputs.tags }}
|
||||||
|
# target: ${{ inputs.target }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
name: Docker Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
image_name:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'solucetechnologies/portabase'
|
||||||
|
image_name2:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'portabase/portabase'
|
||||||
|
add_latest:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
target:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'prod'
|
||||||
|
dockerfile:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: './docker/dockerfile/Dockerfile'
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME:
|
||||||
|
required: true
|
||||||
|
DOCKER_PASSWORD:
|
||||||
|
required: true
|
||||||
|
DOCKER_USERNAME_2:
|
||||||
|
required: true
|
||||||
|
DOCKER_PASSWORD_2:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set tags
|
||||||
|
id: set-tags
|
||||||
|
run: |
|
||||||
|
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
TAGS="${{ inputs.image_name }}:$REF_NAME"
|
||||||
|
TAGS2="${{ inputs.image_name2 }}:$REF_NAME"
|
||||||
|
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
||||||
|
TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
||||||
|
fi
|
||||||
|
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
echo "tags2=$TAGS2" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub account 1
|
||||||
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push to solucetechnologies/portabase
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ inputs.dockerfile }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.set-tags.outputs.tags }}
|
||||||
|
target: ${{ inputs.target }}
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub account 2
|
||||||
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
|
- name: Build and push to portabase/portabase
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ inputs.dockerfile }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.set-tags.outputs.tags2 }}
|
||||||
|
target: ${{ inputs.target }}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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,34 @@
|
|||||||
|
name: Publish Docker image for release candidate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*-rc*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker_publish:
|
||||||
|
uses: ./.github/workflows/docker.yml
|
||||||
|
with:
|
||||||
|
add_latest: false
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
|
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
|
github_release:
|
||||||
|
needs: docker_publish
|
||||||
|
uses: ./.github/workflows/github.yml
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
make_latest: false
|
||||||
|
discord_title: "||@release-dashboard|| New release candidate published"
|
||||||
|
discord_color: 16776960
|
||||||
|
discord_footer: "Portabase"
|
||||||
|
secrets:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,50 +1,35 @@
|
|||||||
name: Publish Docker image for release
|
name: Publish Docker image for release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
- '!*-*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registry:
|
docker_publish:
|
||||||
name: Push Docker image to Docker Hub
|
uses: ./.github/workflows/docker.yml
|
||||||
runs-on: ubuntu-latest
|
with:
|
||||||
permissions:
|
add_latest: true
|
||||||
packages: write
|
secrets:
|
||||||
contents: read
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
attestations: write
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
id-token: write
|
DOCKER_USERNAME_2: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
steps:
|
DOCKER_PASSWORD_2: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
github_release:
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
needs: docker_publish
|
||||||
with:
|
uses: ./.github/workflows/github.yml
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
with:
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
prerelease: false
|
||||||
|
make_latest: true
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
discord_title: "||@release-dashboard|| New release published"
|
||||||
id: meta
|
discord_color: 5814783
|
||||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
discord_footer: "Portabase"
|
||||||
with:
|
secrets:
|
||||||
images: solucetechnologies/portabase
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Set tags
|
|
||||||
id: set-tags
|
|
||||||
run: |
|
|
||||||
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
||||||
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
|
|
||||||
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
|
|
||||||
TAGS="$TAGS,solucetechnologies/portabase:latest"
|
|
||||||
fi
|
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
id: push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./docker/dockerfile/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.set-tags.outputs.tags }}
|
|
||||||
target: prod
|
|
||||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
title: Portabase
|
||||||
|
message: "If you use this software, please cite it as below."
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- family-names: Gauthereau
|
||||||
|
given-names: Charles
|
||||||
|
- family-names: Larcher
|
||||||
|
given-names: Killian
|
||||||
|
repository-code: https://github.com/Portabase/portabase
|
||||||
|
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."
|
||||||
|
keywords:
|
||||||
|
- docker
|
||||||
|
- kubernetes
|
||||||
|
- backups
|
||||||
|
- postgresql
|
||||||
|
- mysql
|
||||||
|
- mariadb
|
||||||
|
- devops
|
||||||
|
- database
|
||||||
|
- monitoring
|
||||||
|
- s3
|
||||||
|
- self-hosted
|
||||||
|
- system-administration
|
||||||
|
- web-ui
|
||||||
|
- agent
|
||||||
|
license: Apache-2.0
|
||||||
|
version: 1.1.12-rc.4
|
||||||
|
date-released: "2026-01-11"
|
||||||
@@ -1,29 +1,39 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://portabase.io">
|
<a href="https://portabase.io">
|
||||||
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
|
<img src="/.github/assets/logo.png" alt="Logo" width="80" height="80">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Portabase</h3>
|
<h3 align="center">Portabase</h3>
|
||||||
<p>
|
|
||||||
Free, open-source, and self-hosted solution for automated backup and restoration of your database instances.
|
<p align="center" style="margin-top: 20px; font-style: italic;">
|
||||||
|
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||||
[](https://github.com/RostislavDugin/postgresus)
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://www.mysql.com/)
|
[](https://www.mysql.com/)
|
||||||
[](https://mariadb.org/)
|
[](https://mariadb.org/)
|
||||||
[](https://github.com/RostislavDugin/postgresus)
|
[](https://github.com/Portabase/portabase)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
|
||||||
|
[![NextJS][NextJS]][NextJS-url]
|
||||||
|
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
|
[![Drizzle][Drizzle]][Drizzle-url]
|
||||||
|
[![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||||
|
[![Docker][Docker]][Docker-url]
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>
|
<strong>
|
||||||
<a href="https://portabase.io">Documentation</a> •
|
<a href="https://portabase.io">Website</a> •
|
||||||
|
<a href="https://portabase.io/docs">Documentation</a> •
|
||||||
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
|
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
|
||||||
<a href="#installation">Installation</a> •
|
<a href="https://portabase.io/docs/dashboard/setup">Installation</a> •
|
||||||
<a href="#contributing">Contributing</a> •
|
|
||||||
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
</strong>
|
</strong>
|
||||||
@@ -34,258 +44,28 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📚 Table of Contents
|
## Installation
|
||||||
|
|
||||||
- [About The Project](#-about-the-project)
|
You have 4 ways to install Portabase:
|
||||||
- [Getting Started](#-getting-started)
|
|
||||||
- [Usage](#-usage)
|
|
||||||
- [Roadmap](#-roadmap)
|
|
||||||
- [Contributing](#-contributing)
|
|
||||||
- [License](#-license)
|
|
||||||
- [Contact](#-contact)
|
|
||||||
- [Acknowledgments](#-acknowledgments)
|
|
||||||
|
|
||||||
---
|
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
||||||
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
|
||||||
|
- Kubernetes with Helm (soon)
|
||||||
|
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
||||||
|
|
||||||
## ✨ About The Project
|
**Ensure Docker is installed on your machine before getting started.**
|
||||||
|
|
||||||
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It
|
|
||||||
integrates seamlessly with Portabase agents for managing operations securely and efficiently.
|
|
||||||
|
|
||||||
GitHub Repository: [Portabase](https://github.com/Portabase/portabase)
|
|
||||||
|
|
||||||
### 🔧 Built With
|
## Contributors
|
||||||
|
|
||||||
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
|
|
||||||
- [![Drizzle][Drizzle]][Drizzle-url]
|
|
||||||
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
|
||||||
- [![BetterAuth][BetterAuth]][BetterAuth-url]
|
|
||||||
- [![Docker][Docker]][Docker-url]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
Ensure Docker is installed on your machine before getting started.
|
|
||||||
|
|
||||||
### Option 1: Docker Compose Setup
|
|
||||||
|
|
||||||
Create a `docker-compose.yml` file with the following configuration:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: portabase
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
portabase:
|
|
||||||
image: solucetechnologies/portabase:latest
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ports:
|
|
||||||
- '8887:80'
|
|
||||||
environment:
|
|
||||||
- TZ="Europe/Paris"
|
|
||||||
volumes:
|
|
||||||
- portabase-private:/app/private
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
container_name: portabase-app
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:17-alpine
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=<your_database>
|
|
||||||
- POSTGRES_USER=<database_user>
|
|
||||||
- POSTGRES_PASSWORD=<database_password>
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD-SHELL", "pg_isready -U <database_user> -d <your_database>" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
portabase-private:
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
If you use reverse proxy like
|
|
||||||
Traefik : [Check this link](https://portabase.io/docs/dashboard/advanced/reverse-proxy)
|
|
||||||
|
|
||||||
### Option 2: Locally (Development)
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Portabase/portabase
|
|
||||||
cd portabase
|
|
||||||
```
|
|
||||||
2. Start the development environment:
|
|
||||||
```bash
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Usage
|
|
||||||
|
|
||||||
Portabase provides a web dashboard to manage your database instances and backups.
|
|
||||||
It currently supports:
|
|
||||||
|
|
||||||
- **PostgreSQL**
|
|
||||||
- **MySQL**
|
|
||||||
|
|
||||||
### Process
|
|
||||||
|
|
||||||
1. **Access the dashboard** – Open `http://localhost:8887` in your browser.
|
|
||||||
2. **Sign up** – Register the first user, who will automatically have the **Admin** role in the default workspace.
|
|
||||||
3. **Add your first agent** – Follow [this guide](https://github.com/Portabase/agent-portabase) for setup
|
|
||||||
instructions.
|
|
||||||
4. **Create organizations and projects** – Link your databases to projects to enable backups and restores.
|
|
||||||
5. **Configure backup policies** – Define schedules (hourly, daily, weekly, or monthly) and retention rules.
|
|
||||||
6. **Choose a storage provider** – Select where backups will be stored (local, S3, etc.).
|
|
||||||
7. **Save and start** – Portabase validates your configuration and starts automated backups based on your defined
|
|
||||||
policies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
|
||||||
|
|
||||||
- [ ] Add changelog
|
|
||||||
- [ ] Enhance documentation
|
|
||||||
- [ ] Implement testing procedures
|
|
||||||
- [ ] Include release file
|
|
||||||
- [ ] Improve security
|
|
||||||
- [ ] Enhance UX/UI
|
|
||||||
- [ ] Improve workspace management system
|
|
||||||
- [x] Migrate to Drizzle ORM
|
|
||||||
- [ ] Add multi-language support
|
|
||||||
- [ ] Extend multi-database support:
|
|
||||||
- [x] PostgreSQL
|
|
||||||
- [ ] MongoDB
|
|
||||||
- [x] MySQL
|
|
||||||
- [x] MariaDB
|
|
||||||
|
|
||||||
Check out [open issues](https://github.com/Portabase/portabase/issues) for more.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions are welcome and appreciated! Here's how to get started:
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a new branch:
|
|
||||||
```bash
|
|
||||||
git checkout -b feature/YourFeature
|
|
||||||
```
|
|
||||||
3. Commit your changes:
|
|
||||||
```bash
|
|
||||||
git commit -m "Add YourFeature"
|
|
||||||
```
|
|
||||||
4. Push to the branch:
|
|
||||||
```bash
|
|
||||||
git push origin feature/YourFeature
|
|
||||||
```
|
|
||||||
5. Open a pull request
|
|
||||||
|
|
||||||
Give the project a ⭐ if you like it!
|
|
||||||
|
|
||||||
### Top Contributors
|
|
||||||
|
|
||||||
[](https://github.com/Portabase/portabase/graphs/contributors)
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Developer Notes
|
## License
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# Environment
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
|
||||||
|
|
||||||
# Project Info
|
|
||||||
PROJECT_NAME="Portabase"
|
|
||||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
|
||||||
PROJECT_URL=http://app.portabase.io
|
|
||||||
PROJECT_SECRET=
|
|
||||||
|
|
||||||
# SMTP (Email)
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=
|
|
||||||
|
|
||||||
# Google OAuth
|
|
||||||
AUTH_GOOGLE_ID=
|
|
||||||
AUTH_GOOGLE_SECRET=
|
|
||||||
AUTH_GOOGLE_METHOD=
|
|
||||||
|
|
||||||
# S3/MinIO Configuration
|
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
|
||||||
S3_ACCESS_KEY=
|
|
||||||
S3_SECRET_KEY=
|
|
||||||
S3_BUCKET_NAME=portabase
|
|
||||||
S3_PORT=9000
|
|
||||||
S3_USE_SSL=true
|
|
||||||
|
|
||||||
# Storage Backend: 'local' or 's3'
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
|
|
||||||
# Retention
|
|
||||||
RETENTION_CRON="* * * * *"
|
|
||||||
```
|
|
||||||
|
|
||||||
To get more information about env variables, check
|
|
||||||
that [link](https://portabase.io/docs/portabase/advanced-topics/environment)
|
|
||||||
|
|
||||||
### Semantic Versioning
|
|
||||||
|
|
||||||
Use the following format for Docker image versioning:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
major.minor.patch-rc.release
|
|
||||||
# Example: 1.0.0-rc.1
|
|
||||||
|
|
||||||
major.minor.patch-rc.release-tag
|
|
||||||
# Example: 1.0.0-rc.1-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📬 Contact
|
|
||||||
|
|
||||||
- Killian Larcher - killian.larcher@soluce-technologies.com
|
|
||||||
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
|
||||||
- Project Link: [Portabase GitHub](https://github.com/Portabase/portabase)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
Thanks to all contributors and the open-source community!
|
|
||||||
|
|
||||||
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
|
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,22 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
{capitalizeFirstLetter(agent.name)}
|
<div className="min-w-full md:min-w-fit ">
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
{capitalizeFirstLetter(agent.name)}
|
||||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
</div>
|
||||||
<GearIcon className="w-7 h-7"/>
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
</Link>
|
<div className="flex items-center gap-2">
|
||||||
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
|
href={`/dashboard/agents/${agent.id}/edit`}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
|
||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agent.description && (
|
{agent.description && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
|||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq, not} from "drizzle-orm";
|
import {desc, eq, not} from "drizzle-orm";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
|
|
||||||
@@ -21,11 +21,10 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
||||||
with: {
|
with: {
|
||||||
databases: true
|
databases: true
|
||||||
}
|
},
|
||||||
|
orderBy: (fields) => desc(fields.lastContact),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(agents);
|
|
||||||
|
|
||||||
|
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
notFound();
|
notFound();
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import {eq} from "drizzle-orm";
|
|||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
|
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||||
|
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
|
||||||
|
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||||
|
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
projectId: string
|
projectId: string
|
||||||
@@ -55,22 +60,25 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
{capitalizeFirstLetter(proj.name)}
|
<div className="min-w-full md:min-w-fit ">
|
||||||
|
{capitalizeFirstLetter(proj.name)}
|
||||||
|
</div>
|
||||||
{!isMember && (
|
{!isMember && (
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
<div className="flex items-center gap-2">
|
||||||
<GearIcon className="w-7 h-7"/>
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
</Link>
|
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
{!isMember && (
|
|
||||||
<PageActions className="justify-between">
|
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
|
||||||
</PageActions>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<PageDescription>The list of associated databases</PageDescription>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
@@ -78,8 +86,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={6}
|
||||||
numberOfColumns={1}
|
numberOfColumns={3}
|
||||||
extendedProps={proj}
|
extendedProps={proj}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
data={projects}
|
data={projects}
|
||||||
cardItem={ProjectCard}
|
cardItem={ProjectCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={6}
|
||||||
numberOfColumns={1}
|
numberOfColumns={3}
|
||||||
/>
|
/>
|
||||||
) : isMember ? (
|
) : isMember ? (
|
||||||
<EmptyStatePlaceholder text="No project available"/>
|
<EmptyStatePlaceholder text="No project available"/>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {Metadata} from "next";
|
|||||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
ButtonDeleteProject
|
||||||
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@@ -26,25 +33,29 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notificationChannels = await getOrganizationChannels(organization.id)
|
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||||
|
|
||||||
const permissions = computeOrganizationPermissions(activeMember);
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
Organization settings
|
<div className="min-w-full md:min-w-fit ">
|
||||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
Organization settings
|
||||||
<EditButtonSettings/>
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{permissions.canManageSettings && organization.slug !== "default" && (
|
||||||
|
<EditButtonSettings/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
||||||
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<PageActions>
|
|
||||||
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
|
||||||
)}
|
|
||||||
</PageActions>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<OrganizationTabs
|
<OrganizationTabs
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export async function POST(
|
|||||||
|
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
const file = formData.get("file") as File | null;
|
const file = formData.get("file") as File | null;
|
||||||
|
const extension = formData.get("extension") as string | null;
|
||||||
if (!aesKeyHex || !ivHex) {
|
if (!aesKeyHex || !ivHex) {
|
||||||
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
||||||
}
|
}
|
||||||
@@ -118,12 +118,13 @@ export async function POST(
|
|||||||
{status: 400}
|
{status: 400}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const fileExtension = getFileExtension(database.dbms)
|
const fileSizeBytes = file.size;
|
||||||
|
// const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || '');
|
||||||
|
const fileExtension = extension ? extension : getFileExtension(database.dbms)
|
||||||
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const fileName = `${uuid}${fileExtension}`;
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
throw new Error("System settings not found.");
|
throw new Error("System settings not found.");
|
||||||
@@ -149,6 +150,7 @@ export async function POST(
|
|||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
file: fileName,
|
file: fileName,
|
||||||
|
fileSize: fileSizeBytes,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
}))
|
}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export async function POST(
|
|||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
const body: BodyResultRestore = await request.json();
|
const body: BodyResultRestore = await request.json();
|
||||||
|
|
||||||
console.log(body)
|
|
||||||
|
|
||||||
if (!isUuidv4(body.generatedId)) {
|
if (!isUuidv4(body.generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {Agent} from "@/db/schema/08_agent";
|
|||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {SafeActionResult} from "next-safe-action";
|
import {SafeActionResult} from "next-safe-action";
|
||||||
@@ -75,8 +75,15 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
const backup = await dbClient.query.backup.findFirst({
|
// const backup = await dbClient.query.backup.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
// where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||||
|
// })
|
||||||
|
|
||||||
|
const activeBackup = await dbClient.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
|
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -85,13 +92,13 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (backup) {
|
if (activeBackup && activeBackup.status == "waiting") {
|
||||||
backupAction = true
|
backupAction = true
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set(withUpdatedAt({status: "ongoing"}))
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, activeBackup.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoration) {
|
if (restoration) {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export async function POST(
|
|||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUuidv4(agentId)) {
|
||||||
message = "agentId is not a valid uuid"
|
message = "agentId is not a valid uuid"
|
||||||
console.log(message)
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{error: "agentId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -54,7 +53,6 @@ export async function POST(
|
|||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
message = "Agent not found"
|
message = "Agent not found"
|
||||||
console.log(message)
|
|
||||||
return NextResponse.json({error: message}, {status: 404})
|
return NextResponse.json({error: message}, {status: 404})
|
||||||
}
|
}
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
||||||
@@ -77,7 +75,6 @@ export async function POST(
|
|||||||
},
|
},
|
||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
console.log(response)
|
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export async function GET(request: Request) {
|
|||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
console.log('Stream started');
|
console.log('Stream started');
|
||||||
|
|
||||||
const handleModification = (data: any) => {
|
const handleModification = (data: any) => {
|
||||||
console.log('Modification event triggered:', data);
|
console.log('Modification event triggered:', data);
|
||||||
controller.enqueue(`event: modification\n`);
|
controller.enqueue(`event: modification\n`);
|
||||||
@@ -46,14 +45,3 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// export async function POST(request: Request) {
|
|
||||||
// console.log('POST request received');
|
|
||||||
// const data = await request.json();
|
|
||||||
// console.log('Data received:', data);
|
|
||||||
//
|
|
||||||
// // Emit the event to all connected clients
|
|
||||||
// eventEmitter.emit('modification', data);
|
|
||||||
//
|
|
||||||
// return new Response('Event sent', {status: 200});
|
|
||||||
// }
|
|
||||||
@@ -1,45 +1,37 @@
|
|||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
ENV YARN_VERSION=4.9.1
|
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
tzdata \
|
|
||||||
build-base \
|
|
||||||
libc6-compat \
|
libc6-compat \
|
||||||
openssl \
|
openssl \
|
||||||
make \
|
tzdata
|
||||||
|
|
||||||
|
FROM base AS build-env
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache \
|
||||||
|
build-base \
|
||||||
g++ \
|
g++ \
|
||||||
jpeg-dev \
|
make \
|
||||||
cairo-dev \
|
|
||||||
giflib-dev \
|
|
||||||
pango-dev \
|
|
||||||
libtool \
|
libtool \
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake
|
||||||
libpng
|
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
RUN yarn set version ${YARN_VERSION}
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS deps
|
|
||||||
|
|
||||||
|
FROM build-env AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY .yarn ./.yarn
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
COPY .yarnrc.yml ./
|
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
RUN pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
if [ -f yarn.lock ]; then yarn; \
|
|
||||||
elif [ -f package-lock.json ]; then npm ci; \
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
|
||||||
else echo "Lockfile not found." && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS dev
|
FROM build-env AS dev
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -52,8 +44,11 @@ RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
|||||||
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
|
||||||
FROM base AS builder
|
|
||||||
|
|
||||||
|
|
||||||
|
FROM build-env AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
@@ -61,12 +56,11 @@ COPY . .
|
|||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN \
|
RUN pnpm run build
|
||||||
if [ -f yarn.lock ]; then yarn run build; \
|
|
||||||
elif [ -f package-lock.json ]; then npm run build; \
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
|
||||||
else echo "Lockfile not found." && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS prod
|
FROM base AS prod
|
||||||
@@ -109,5 +103,3 @@ ENV HOSTNAME="0.0.0.0"
|
|||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "▶ Running Drizzle codegen..."
|
echo "▶ Running Drizzle codegen..."
|
||||||
npx drizzle-kit generate
|
pnpm drizzle-kit generate
|
||||||
|
|
||||||
echo "▶ Applying migrations..."
|
echo "▶ Applying migrations..."
|
||||||
npx drizzle-kit migrate
|
pnpm drizzle-kit migrate
|
||||||
|
|
||||||
echo "▶ Starting Next.js dev server..."
|
echo "▶ Starting Next.js dev server..."
|
||||||
exec npm run dev
|
exec pnpm dev
|
||||||
+3
-1
@@ -43,8 +43,10 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "50mb",
|
bodySizeLimit: "10gb",
|
||||||
},
|
},
|
||||||
|
proxyClientMaxBodySize: '10gb',
|
||||||
|
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.1.6",
|
"version": "1.1.12-rc.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
|
"@react-email/render": "^2.0.1",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.15.18",
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"eslint-config-next": "^16.0.1",
|
"eslint-config-next": "^16.0.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||||
|
"framer-motion": "^12.24.7",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
@@ -118,5 +120,5 @@
|
|||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"zenstack": "2.14.2"
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1"
|
"packageManager": "pnpm@10.27.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+14158
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- zenstack
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: ./release <version>"
|
||||||
|
echo "Example: ./release v1.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!"
|
||||||
@@ -63,7 +63,6 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
toast.success("Login success");
|
toast.success("Login success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
toast.error(error.error.message);
|
toast.error(error.error.message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const resetPasswordAction = action
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
||||||
console.log(user)
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -39,28 +38,6 @@ export const resetPasswordAction = action
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await (await auth.$context).password.hash(parsedInput.schema.password);
|
|
||||||
console.log(hashedPassword)
|
|
||||||
console.log("ok")
|
|
||||||
// await (await auth.$context).internalAdapter.updatePassword(user.id, hashedPassword);
|
|
||||||
console.log("ici")
|
|
||||||
// await (await auth.$context).internalAdapter.deleteSessions(user.id);
|
|
||||||
// console.log("ici2")
|
|
||||||
// await (await auth.$context).internalAdapter.deleteVerificationValue(verification.id);
|
|
||||||
// console.log("ici3");
|
|
||||||
//
|
|
||||||
// (await auth.$context).internalAdapter.updateUser(user.id, {
|
|
||||||
// lastChangedPasswordAt: new Date(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await auth.api.resetPassword({
|
|
||||||
// headers: await headers(),
|
|
||||||
// body: {
|
|
||||||
// newPassword: parsedInput.schema.password,
|
|
||||||
// token: parsedInput.token,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
await (
|
await (
|
||||||
await auth.$context
|
await auth.$context
|
||||||
).internalAdapter.updateUser(user.id, {
|
).internalAdapter.updateUser(user.id, {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
|
|||||||
setTimeout(() => router.push("/"), 1400);
|
setTimeout(() => router.push("/"), 1400);
|
||||||
},
|
},
|
||||||
onError: (error: BetterAuthError) => {
|
onError: (error: BetterAuthError) => {
|
||||||
console.log(error)
|
|
||||||
toast.error("An error occurred while resetting password");
|
toast.error("An error occurred while resetting password");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export const RegisterForm = (props: registerFormProps) => {
|
|||||||
router.push(`/login`);
|
router.push(`/login`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
toast.error(error.error.message);
|
toast.error(error.error.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {Form, FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/c
|
|||||||
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
|
||||||
import {ResetPasswordSchema, ResetPasswordType} from "@/components/wrappers/auth/reset-password/reset-password-schema";
|
import {ResetPasswordSchema, ResetPasswordType} from "@/components/wrappers/auth/reset-password/reset-password-schema";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
@@ -31,7 +31,6 @@ export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
|
|||||||
token,
|
token,
|
||||||
}, {
|
}, {
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
console.log(response);
|
|
||||||
toast.success("Password changed successfully.");
|
toast.success("Password changed successfully.");
|
||||||
setTimeout(() => router.push("/"), 1000);
|
setTimeout(() => router.push("/"), 1000);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
|||||||
const timestamp = date.getTime();
|
const timestamp = date.getTime();
|
||||||
const interval_seconds = (now - timestamp) / 1000;
|
const interval_seconds = (now - timestamp) / 1000;
|
||||||
|
|
||||||
console.log({now, timestamp, interval_seconds});
|
|
||||||
|
|
||||||
if (interval_seconds < 55) {
|
if (interval_seconds < 55) {
|
||||||
style = "bg-green-400 border-green-600";
|
style = "bg-green-400 border-green-600";
|
||||||
} else if (interval_seconds <= 60) {
|
} else if (interval_seconds <= 60) {
|
||||||
@@ -25,7 +23,5 @@ export const ConnectionCircle = ({date}: ConnectionCircleProps) => {
|
|||||||
console.warn("Invalid date passed to ConnectionCircle:", date);
|
console.warn("Invalid date passed to ConnectionCircle:", date);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(style);
|
|
||||||
|
|
||||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
return <div className={cn("w-5 h-5 rounded-full border-4", style)}/>;
|
||||||
};
|
};
|
||||||
|
|||||||
-4
@@ -29,10 +29,6 @@ export const NotifierChannelOrganisationForm = ({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
console.log("defaultValues", defaultValues);
|
|
||||||
console.log("organizations", organizations);
|
|
||||||
|
|
||||||
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
const defaultOrganizationIds = defaultValues?.organizations?.map(organization => organization.organizationId) ?? []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Mail, MessageSquare, Webhook} from "lucide-react";
|
import {Mail} from "lucide-react";
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
export const getNotificationChannelIcon = (type: string) => {
|
export const getNotificationChannelIcon = (type: string) => {
|
||||||
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
const Icon = notificationTypes.find((t) => t.value === type)?.icon
|
||||||
@@ -6,8 +7,481 @@ export const getNotificationChannelIcon = (type: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const notificationTypes = [
|
export const notificationTypes = [
|
||||||
{value: "smtp", label: "Email", icon: Mail},
|
{value: "smtp", label: "Email", icon: Mail},
|
||||||
{value: "slack", label: "Slack", icon: MessageSquare},
|
{value: "slack", label: "Slack", icon: SlackIcon},
|
||||||
{value: "webhook", label: "Webhook", icon: Webhook},
|
{value: "discord", label: "Discord", icon: DiscordIcon},
|
||||||
|
{value: "telegram", label: "Telegram", icon: TelegramIcon},
|
||||||
|
{value: "gotify", label: "Gotify", icon: GotifyIcon},
|
||||||
|
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
|
||||||
|
{value: "webhook", label: "Webhook", icon: WebhookIcon},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export function SlackIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><path fill="#e01e5a" d="M53.841 161.32c0 14.832-11.987 26.82-26.819 26.82S.203 176.152.203 161.32c0-14.831 11.987-26.818 26.82-26.818H53.84zm13.41 0c0-14.831 11.987-26.818 26.819-26.818s26.819 11.987 26.819 26.819v67.047c0 14.832-11.987 26.82-26.82 26.82c-14.83 0-26.818-11.988-26.818-26.82z"></path><path fill="#36c5f0" d="M94.07 53.638c-14.832 0-26.82-11.987-26.82-26.819S79.239 0 94.07 0s26.819 11.987 26.819 26.819v26.82zm0 13.613c14.832 0 26.819 11.987 26.819 26.819s-11.987 26.819-26.82 26.819H26.82C11.987 120.889 0 108.902 0 94.069c0-14.83 11.987-26.818 26.819-26.818z"></path><path fill="#2eb67d" d="M201.55 94.07c0-14.832 11.987-26.82 26.818-26.82s26.82 11.988 26.82 26.82s-11.988 26.819-26.82 26.819H201.55zm-13.41 0c0 14.832-11.988 26.819-26.82 26.819c-14.831 0-26.818-11.987-26.818-26.82V26.82C134.502 11.987 146.489 0 161.32 0s26.819 11.987 26.819 26.819z"></path><path fill="#ecb22e" d="M161.32 201.55c14.832 0 26.82 11.987 26.82 26.818s-11.988 26.82-26.82 26.82c-14.831 0-26.818-11.988-26.818-26.82V201.55zm0-13.41c-14.831 0-26.818-11.988-26.818-26.82c0-14.831 11.987-26.818 26.819-26.818h67.25c14.832 0 26.82 11.987 26.82 26.819s-11.988 26.819-26.82 26.819z"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} viewBox="0 0 256 199" {...props}><path fill="#5865f2" d="M216.856 16.597A208.5 208.5 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046q-29.538-4.442-58.533 0c-1.832-4.4-4.55-9.933-6.846-14.046a207.8 207.8 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161 161 0 0 0 79.735 175.3a136.4 136.4 0 0 1-21.846-10.632a109 109 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a132 132 0 0 0 5.355 4.237a136 136 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848c21.142-6.58 42.646-16.637 64.815-33.213c5.316-56.288-9.08-105.09-38.056-148.36M85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2s23.236 11.804 23.015 26.2c.02 14.375-10.148 26.18-23.015 26.18m85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2c0 14.375-10.148 26.18-23.015 26.18"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (<svg xmlns="http://www.w3.org/2000/svg" width={256} height={256} viewBox="0 0 256 256" {...props}><defs><linearGradient id="SVGuySfwdaH" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stopColor="#2aabee"></stop><stop offset="100%" stopColor="#229ed9"></stop></linearGradient></defs><path fill="url(#SVGuySfwdaH)" d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"></path><path fill="#fff" d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"></path></svg>);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function NtfyIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
|
||||||
|
return ( <svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
id="Layer_1"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>{".st1{fill:#fff}"}</style>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_1_"
|
||||||
|
x1={115.045}
|
||||||
|
x2={142.93}
|
||||||
|
y1={512.747}
|
||||||
|
y2={491.741}
|
||||||
|
gradientTransform="matrix(15.8584 0 0 -15.8584 -1816.246 8200.18)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#348878",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#56bda8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M441.8 64.8H84.7c-24.5 0-44.6 19-44.6 42.1l.3 285.3-6.7 38.4 95.6-28.8h312.4c24.5 0 44.6-19 44.6-42.1V106.9c.1-23.2-20-42.1-44.5-42.1"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_1_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.1 32.7C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5zm0 46.9H443c13.1 0 22.1 9.5 22.1 19.6V379c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M116.4 310.9v-34.8l73.1-38.7c2.4-1.2 4.6-2.1 6.8-2.7 2.3-.8 4.5-1.4 6.3-1.8l6-.6V230q-3-.3-6-1.2-2.85-.6-6.3-1.5c-2.2-.8-4.5-1.8-6.8-3l-73.1-38.7v-35.1l113 61.5v37.4z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M110.5 139.7v50.4l76.6 40.6.5.2-.5.2-76.6 40.6v50.1l8.2-4.5L235.1 254v-46.4zm11.8 21.7 101.2 55.1V245l-101.2 55.1v-19.4l69.6-36.8.1-.1c1.9-1 3.8-1.7 5.8-2.3h.1l.2-.1c2.1-.7 3.9-1.2 5.5-1.6l11.1-1.1v-15.2l-5.4-.5c-1.7-.2-3.5-.5-5.1-1.1h-.2l-.2-.1c-2-.4-3.9-.9-5.9-1.4-2-.7-4-1.6-5.9-2.6l-.1-.1-69.6-36.7zm152.3 137.4h120.7v32.6H274.6z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M268.7 292.1v46h132.4v-46zm11.7 13.4h109v19.2h-109z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
</svg>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function GotifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
id="Layer_2"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
viewBox="46.6 113.46 377.2 307.81"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<style>
|
||||||
|
{
|
||||||
|
".st0{fill:#ddcba2}.st0,.st1,.st2,.st4,.st5{stroke:#000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10}.st1{fill:#71caee}.st2{fill:#fff}.st4{fill:#f0f0f0}.st5{fill:none}"
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<switch>
|
||||||
|
<foreignObject
|
||||||
|
width={1}
|
||||||
|
height={1}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M317.7 376.2c6.2-1.7 15.8 0 19.5 3.2 5.5 4.8 4.9 20.9 1.1 29-.8 1.7-1.8 3.4-3.2 4.4s-3.4 1.2-4.7 0-1.7-3.3-1.9-5.2c-.2-3.1-.2-6.2.3-9.2.2-1.3.2-2.9-.2-4.2-.6-2.2-2.5-3.1-4.5-3.5-3.4-.8-14.3-.7-19.5-.7-.8 0 1.5-9.1 2-9.7 2.6-2.9 7.5-3.1 11.1-4.1z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m258.9 119.7-9-2.7c-4.6-1.4-9.2-2.8-14-2.5-2.8.2-6.1 1.3-6.9 4-.6 2-1.6 7.3-1.3 7.9 1.5 3.4 13.9 6.7 18.3 6.7"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path d="M392.6 177.9c-1.4 1.4-2.2 3.5-2.5 5.5-.2 1.4-.1 3 .5 4.3s1.8 2.3 3.1 3c1.3.6 2.8.9 4.3.9 1.1 0 2.3-.1 3.1-.9.6-.7.8-1.6.9-2.5.2-2.3-.1-4.7-.9-6.9-.4-1.1-.9-2.3-1.8-3.1-1.7-1.8-4.5-2.2-6.4-.5-.1 0-.2.1-.3.2" />
|
||||||
|
<path
|
||||||
|
d="M358.5 164.2c-1-1 0-2.7 1-3.7 5.8-5.2 15.1-4.6 21.8-.6 10.9 6.6 15.6 19.9 17.2 32.5.6 5.2.9 10.6-.5 15.7s-4.6 9.9-9.3 12.1c-1.1.5-2.3.9-3.4.5s-1.9-1.8-1.2-2.8c-9.4-13.6-19-26.8-20.9-43.2-.5-4.1-1.8-7.4-4.7-10.5z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M134.7 328.4c-5.1-3.1-9.9-6.6-14.3-10.6-1.3-1.2-2.6-2.5-2.6-4.3 0-1.2.6-2.2 1.2-3.2.8-1.4 1.7-2.8 2.5-4.1 1.1-1.8 2.9-3.9 4.9-3.2.9.3 1.5 1.1 2 1.8 2.4 3.3 4.9 6.6 7.3 9.8 1.5 2 3.7 4.3 6.1 3.5"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M209.6 133c33.2-18 77.8-19.6 111.5-8.7 24.3 7.9 43.4 26.7 53.3 50 8.7 20.6 10.5 43.6 8.1 65.7-4.4 40.2-20.2 77.9-40.3 112.6-11.1 19-21.8 36-40.5 48.5-36.8 24.6-87.2 22.1-128.4 11.5-19.9-5.1-39.7-17.3-47.2-37.3-4.8-12.8-4.2-27.6 1.5-40 11.6-24.8 43.2-38.4 45.6-67.9.7-8.7-1.6-17.3-3.6-25.7-5.6-23.4-8.9-45.8 1.4-68.7 8.1-17.7 21.9-31 38.6-40z"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M189.8 151.4c-5.4-5.2-11.9-8.8-19-10.3-2.2-.5-4.7-.7-6.9.7-1.8 1.2-3.1 3.3-4.2 5.3-1.6 3-3 6.2-4.1 9.4-.4 1.2-.6 2.5 0 3.5.3.6.9.9 1.5 1.2 8.1 4.2 16.8 7.1 25.5 9.8"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M183.7 158.7c-2.5-1.8-16.8-12.1-18.7-4.8-.4 1.6.5 3.9 1.5 4.8"
|
||||||
|
className="st1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M264.5 174.9c-.5.5-.9 1-1.3 1.6-9 11.6-12 27.9-9.3 42.1 1.7 9 5.9 17.9 13.2 23.4 19.3 14.6 51.5 13.5 68.4-1.5 24.4-21.7 13-67.6-14-78.8-17.6-7.2-43.7-1.6-57 13.2zm117.6 62.2c1.4-.1 2.9-.1 4.3.1.3 0 .7.1 1 .4.2.3.4.7.5 1.1 1 3.9.5 8.2.1 12.4-.1.9-.2 1.8-.6 2.6-1 2.1-3.1 2.7-4.7 2.7-.1 0-.2 0-.3-.1-.3-.2-.3-.7-.2-1.2.3-5.9-.1-11.9-.1-18q0 .15 0 0z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M378.7 236.8c-1.4.4-2.5 2-2.8 4.4-.5 4.4-.7 8.9-.5 13.4 0 .9.1 1.9.5 2.4.2.3.5.4.8.4 1.6.3 4.1-.6 5.6-1 0 0 0-5.2-.1-8s-.1-6.1-.2-8.9v-2.2c.1-.7-2.6-.7-3.3-.5z"
|
||||||
|
className="st2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M358.3 231.8c-.3 2.2.1 4.7 1.7 7.4 2.6 4.4 7 6.1 11.9 5.8 8.9-.6 25.3-5.4 27.5-15.7.6-3-.3-6.1-2.2-8.5-6.2-7.8-17.8-5.7-25.6-2-5.9 2.7-12.4 7-13.3 13z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M386.4 208.6c2.2 1.4 3.7 3.8 4 7 .3 3.6-1.4 7.5-5 8.8-2.9 1.1-6.2.6-9.1-.4s-5.8-2.8-6.8-5.7c-.7-2-.3-4.3.7-6.1 1.1-1.8 2.8-3.2 4.7-4.1 3.9-1.8 8.4-1.6 11.5.5z"
|
||||||
|
style={{
|
||||||
|
fill: "#888e93",
|
||||||
|
stroke: "#000",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeMiterlimit: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M414.7 262.6c2.4.6 4.8 2.1 5.6 4.4s.1 4.9-1.6 6.7-4.2 2.5-6.6 2.5c-.8 0-1.7-.1-2.4-.5-2.5-1.1-3.5-4-4.2-6.6-1.8-6.8 3.6-7.8 9.2-6.5z"
|
||||||
|
className="st0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M267.1 284.7c2.3-4.5 141.3-36.2 144.7-31.6 3.4 4.5 15.8 88.2 9 90.4-6.8 2.3-119.8 37.3-126.6 35s-29.4-89.3-27.1-93.8z"
|
||||||
|
className="st4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M294.2 378.5s54.3-74.6 59.9-76.9c5.7-2.3 67.3 41.3 67.3 41.3"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M267 287.7s86 38.8 91.6 36.6c5.7-2.3 53.1-71.2 53.1-71.2"
|
||||||
|
className="st4"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_1_"
|
||||||
|
x1={131.119}
|
||||||
|
x2={103.259}
|
||||||
|
y1={373.573}
|
||||||
|
y2={396.951}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#71caee",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.041}
|
||||||
|
style={{
|
||||||
|
stopColor: "#83cae2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.121}
|
||||||
|
style={{
|
||||||
|
stopColor: "#9fcace",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.211}
|
||||||
|
style={{
|
||||||
|
stopColor: "#b6cbbe",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.314}
|
||||||
|
style={{
|
||||||
|
stopColor: "#c7cbb1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.438}
|
||||||
|
style={{
|
||||||
|
stopColor: "#d4cba8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.606}
|
||||||
|
style={{
|
||||||
|
stopColor: "#dbcba3",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#ddcba2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_1_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M132.8 375.6c-3.5 3.8-7.3 7.8-13 9.2-4.6 1.2-10 .2-13.6-2.3-1.4-1-2.6-2.2-4-3.2-1.5-1-3.4-1.7-5.3-1.3-2.7.5-4.1 3.1-3.6 5.3 2 8.8 17 15.6 27.5 15.5 9 0 19-4.6 21.4-11.8"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_2_"
|
||||||
|
x1={265.033}
|
||||||
|
x2={275.301}
|
||||||
|
y1={280.295}
|
||||||
|
y2={302.314}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#71caee",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.041}
|
||||||
|
style={{
|
||||||
|
stopColor: "#83cae2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.121}
|
||||||
|
style={{
|
||||||
|
stopColor: "#9fcace",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.211}
|
||||||
|
style={{
|
||||||
|
stopColor: "#b6cbbe",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.314}
|
||||||
|
style={{
|
||||||
|
stopColor: "#c7cbb1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.438}
|
||||||
|
style={{
|
||||||
|
stopColor: "#d4cba8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.606}
|
||||||
|
style={{
|
||||||
|
stopColor: "#dbcba3",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#ddcba2",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_2_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M261.9 283.5c-.1 4.2 4.3 7.3 8.4 7.6s8.2-1.3 12.2-2.6c1.4-.4 2.9-.8 4.2-.2 1.8.9 2.7 4.1 1.8 5.9s-3.4 3.5-5.3 4.4c-6.5 3-12.9 3.6-19.9 2-5.3-1.2-11.3-4.3-13-13.5"
|
||||||
|
className="st5"
|
||||||
|
/>
|
||||||
|
<path d="M318.4 198.4c-2-.3-4.1.1-5.9 1.3-3.2 2.1-4.7 6.2-4.7 9.9 0 1.9.4 3.8 1.4 5.3 1.2 1.7 3.1 2.9 5.2 3.4 3.4.8 8.2.7 10.5-2.5 1-1.5 1.4-3.3 1.5-5.1.5-5.7-1.8-11.4-8-12.3" />
|
||||||
|
<path
|
||||||
|
d="M320.4 203.3c.9.3 1.7.8 2.1 1.7.4.8.4 1.7.3 2.5-.1 1-.6 2-1.5 2.7-.7.5-1.7.7-2.6.5s-1.7-.8-2.2-1.6c-1.1-1.6-.9-4.4.9-5.5.9-.4 2-.6 3-.3"
|
||||||
|
style={{
|
||||||
|
fill: "#fff",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_3_"
|
||||||
|
x1={-18.456}
|
||||||
|
x2={110.382}
|
||||||
|
y1={252.286}
|
||||||
|
y2={252.286}
|
||||||
|
gradientTransform="rotate(-5.297 -26.409 -856.73)scale(.99997)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M81.6 222.4s56.5 68.4 123.9 42.4l6.6-29.7C150 272.2 82.8 214.3 82.8 214.3z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_3_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_4_"
|
||||||
|
x1={110.198}
|
||||||
|
x2={283.737}
|
||||||
|
y1={262.493}
|
||||||
|
y2={262.493}
|
||||||
|
gradientTransform="matrix(.9991 .04164 -.04164 .9991 -36.323 1.051)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M64.2 234.9s63.1 101.5 157.8 78.9l14.2-38.5c-89.5 38.3-168.9-51-168.9-51z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_4_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_5_"
|
||||||
|
x1={218.967}
|
||||||
|
x2={428.745}
|
||||||
|
y1={263.594}
|
||||||
|
y2={263.594}
|
||||||
|
gradientTransform="rotate(9.265 -38.832 -826.002)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset={0}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.477}
|
||||||
|
style={{
|
||||||
|
stopColor: "#575756",
|
||||||
|
stopOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={0.995}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0.001911551,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={1}
|
||||||
|
style={{
|
||||||
|
stopColor: "#333",
|
||||||
|
stopOpacity: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M46.6 251.5s61.1 130.9 178 117.5l22.6-44.2c-113 33-195.3-85.7-195.3-85.7z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#SVGID_5_)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</switch>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebhookIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={2500}
|
||||||
|
height={2448}
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit={2}
|
||||||
|
clipRule="evenodd"
|
||||||
|
viewBox="-6.74 -6.74 223.55 218.799"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#7547db"
|
||||||
|
d="M111.134 71.906a21 21 0 0 1-1.324.042c-11.314 0-20.5-9.186-20.5-20.5s9.186-20.5 20.5-20.5 20.5 9.186 20.5 20.5a20.4 20.4 0 0 1-4.582 12.913l22.66 41.655a51 51 0 0 1 17.422-3.068c28.148 0 51 22.852 51 51s-22.852 51-51 51a51.2 51.2 0 0 1-28.036-8.373 7.303 7.303 0 0 1-1.359-11.105c.015-.016.031-.033.061-.036a7.665 7.665 0 0 1 9.736-1.228 36.06 36.06 0 0 0 19.598 5.792c19.897 0 36.05-16.153 36.05-36.05s-16.153-36.05-36.05-36.05c-6.89 0-16.669 3.014-22.548 6.745a2.637 2.637 0 0 1-3.899-.845zm-9.412 88.614a51 51 0 0 1-6.054 16.62c-14.074 24.378-45.29 32.743-69.667 18.669S-6.74 150.518 7.333 126.14a51.2 51.2 0 0 1 21.27-20.094 7.3 7.3 0 0 1 6.167-.211 7.3 7.3 0 0 1 4.13 4.587c.006.022.012.043 0 .07a7.665 7.665 0 0 1-3.804 9.047 36.05 36.05 0 0 0-14.816 14.076c-9.948 17.23-4.036 39.297 13.195 49.246 17.231 9.948 39.298 4.035 49.246-13.196 3.445-5.967 5.724-15.943 5.433-22.9a2.632 2.632 0 0 1 2.681-2.953l59.054 1.499q.292-.59.626-1.168c5.657-9.798 18.205-13.161 28.003-7.504 9.8 5.658 13.161 18.206 7.504 28.004s-18.205 13.16-28.004 7.503a20.4 20.4 0 0 1-8.892-10.424zM76.97 90.555A51 51 0 0 1 65.601 77c-14.074-24.377-5.71-55.594 18.667-69.667S139.862 1.624 153.935 26a51.2 51.2 0 0 1 6.768 28.466 7.306 7.306 0 0 1-8.938 6.73c-.022-.005-.044-.011-.062-.035a7.664 7.664 0 0 1-5.932-7.817 36.04 36.04 0 0 0-4.783-19.87c-9.948-17.23-32.014-23.143-49.245-13.195S68.599 52.294 78.548 69.525c3.445 5.967 10.945 12.929 17.115 16.155a2.635 2.635 0 0 1 1.218 3.799l-30.826 50.393q.366.548.7 1.125c5.656 9.799 2.294 22.347-7.505 28.004s-22.346 2.295-28.003-7.504-2.295-22.346 7.503-28.003a20.4 20.4 0 0 1 13.474-2.488z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
-3
@@ -34,13 +34,11 @@ export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}
|
|||||||
|
|
||||||
const mutationAddMemberOrganisation = useMutation({
|
const mutationAddMemberOrganisation = useMutation({
|
||||||
mutationFn: async (data: AddMemberSchemaType) => {
|
mutationFn: async (data: AddMemberSchemaType) => {
|
||||||
console.log(data);
|
|
||||||
const result = await addMemberOrganizationAction({
|
const result = await addMemberOrganizationAction({
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
role: "member",
|
role: "member",
|
||||||
});
|
});
|
||||||
console.log(result);
|
|
||||||
toast.success("Member successfully added!");
|
toast.success("Member successfully added!");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
onSuccessAction?.();
|
onSuccessAction?.();
|
||||||
@@ -71,7 +69,6 @@ export const OrganizationAddMemberForm = ({onSuccessAction, users, organization}
|
|||||||
placeholder="Enter a user email"
|
placeholder="Enter a user email"
|
||||||
entries={filteredUsers}
|
entries={filteredUsers}
|
||||||
onSelect={(entySelected: any) => {
|
onSelect={(entySelected: any) => {
|
||||||
console.log("Form selection:", entySelected);
|
|
||||||
field.onChange(entySelected.value);
|
field.onChange(entySelected.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
-2
@@ -35,13 +35,11 @@ export const OrganizationDeleteMemberModal = ({ member, open, onOpenChangeAction
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
console.log(response);
|
|
||||||
toast.success("Member successfully deleted!");
|
toast.success("Member successfully deleted!");
|
||||||
onOpenChangeAction(false);
|
onOpenChangeAction(false);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: async (error) => {
|
onError: async (error) => {
|
||||||
console.log(error);
|
|
||||||
toast.error("An error occurred while deleting member!");
|
toast.error("An error occurred while deleting member!");
|
||||||
onOpenChangeAction(false);
|
onOpenChangeAction(false);
|
||||||
},
|
},
|
||||||
|
|||||||
-1
@@ -49,7 +49,6 @@ export const OrganizationMemberChangeRoleModal = (props: OrganizationMemberChang
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
toast.error("An error occurred while updating member");
|
toast.error("An error occurred while updating member");
|
||||||
onOpenChangeAction(false);
|
onOpenChangeAction(false);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,8 +95,6 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4 mt-3"
|
className="flex flex-col gap-4 mt-3"
|
||||||
@@ -182,23 +180,6 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
|
|
||||||
{/*{props.defaultValues?.smtpFrom && (*/}
|
|
||||||
{/* <ButtonWithLoading*/}
|
|
||||||
{/* type="button"*/}
|
|
||||||
{/* isPending={mutationSendEmailTest.isPending}*/}
|
|
||||||
{/* onClick={async () => {*/}
|
|
||||||
{/* await mutationSendEmailTest.mutateAsync();*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* icon={<Send/>}*/}
|
|
||||||
{/* size="default"*/}
|
|
||||||
{/* className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"*/}
|
|
||||||
|
|
||||||
{/* >Send email test</ButtonWithLoading>*/}
|
|
||||||
{/*)}*/}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{props.defaultValues?.smtpFrom && (
|
{props.defaultValues?.smtpFrom && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -227,14 +208,13 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button type="submit">
|
||||||
type="submit"
|
Save
|
||||||
>Save</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
-2
@@ -28,11 +28,9 @@ export const StorageS3Form = (props: S3FormProps) => {
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: S3FormType) => {
|
mutationFn: async (values: S3FormType) => {
|
||||||
console.log(values);
|
|
||||||
const updateS3Settings = await updateS3SettingsAction({ name: "system", data: values });
|
const updateS3Settings = await updateS3SettingsAction({ name: "system", data: values });
|
||||||
const data = updateS3Settings?.data?.data;
|
const data = updateS3Settings?.data?.data;
|
||||||
if (updateS3Settings?.serverError || !data) {
|
if (updateS3Settings?.serverError || !data) {
|
||||||
console.log(updateS3Settings?.serverError);
|
|
||||||
toast.error(updateS3Settings?.serverError);
|
toast.error(updateS3Settings?.serverError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const AdminUserEditForm = ({ onSuccess, defaultValues }: AdminUserEditFor
|
|||||||
...data,
|
...data,
|
||||||
id: defaultValues?.id || "",
|
id: defaultValues?.id || "",
|
||||||
});
|
});
|
||||||
console.log(result)
|
|
||||||
const inner = result?.data;
|
const inner = result?.data;
|
||||||
if (inner?.success) {
|
if (inner?.success) {
|
||||||
toast.success("User Successfully updated");
|
toast.success("User Successfully updated");
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {useState} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||||
import {Agent, AgentWith} from "@/db/schema/08_agent";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Activity, Database, ShieldCheck} from "lucide-react";
|
import {AgentWith} from "@/db/schema/08_agent";
|
||||||
|
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, ShieldCheck} from "lucide-react";
|
||||||
|
import {Badge} from "@/components/ui/badge";
|
||||||
|
import {truncateWords} from "@/utils/text";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type agentCardProps = {
|
export type agentCardProps = {
|
||||||
data: AgentWith;
|
data: AgentWith;
|
||||||
@@ -13,39 +17,79 @@ export type agentCardProps = {
|
|||||||
|
|
||||||
export const AgentCard = (props: agentCardProps) => {
|
export const AgentCard = (props: agentCardProps) => {
|
||||||
const {data: agent} = props;
|
const {data: agent} = props;
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
navigator.clipboard.writeText(agent.id);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/agents/${agent.id}`}
|
<Link
|
||||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
href={`/dashboard/agents/${agent.id}`}
|
||||||
|
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||||
>
|
>
|
||||||
<Card className="flex flex-row justify-between">
|
<Card className="flex flex-row items-center p-4 gap-5 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-md overflow-hidden">
|
||||||
<div className="flex-1 text-left">
|
<div className="relative w-12 h-12 p-2.5 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105 shrink-0">
|
||||||
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
|
<Server className="w-6 h-6 text-foreground/80 group-hover:text-primary transition-colors" />
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Activity className="h-4 w-4"/>
|
|
||||||
<span>{formatDateLastContact(agent.lastContact)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-4 w-4"/>
|
|
||||||
<span>{agent.databases?.length ?? 0} DB</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ShieldCheck className="h-4 w-4"/>
|
|
||||||
{agent.version ?
|
|
||||||
<span>v{agent.version}</span>
|
|
||||||
:
|
|
||||||
<span>N/A</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center px-4">
|
|
||||||
<ConnectionCircle date={agent.lastContact}/>
|
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight ">
|
||||||
|
{isMobile ? truncateWords(agent.name, 2) : agent.name}
|
||||||
|
</h3>
|
||||||
|
{agent.version && (
|
||||||
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
|
||||||
|
v{agent.version}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<div className="hidden sm:flex items-center gap-1.5 min-w-0">
|
||||||
|
<Fingerprint className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
<span className="font-mono text-xs font-bold truncate opacity-80">
|
||||||
|
{agent.id}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="ml-0.5 p-1 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10"
|
||||||
|
title="Copy ID"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<Database className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-tight opacity-80">
|
||||||
|
{agent.databases?.length ?? 0} DBs
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 shrink-0">
|
||||||
|
<div className="hidden sm:flex flex-col items-end gap-1 text-right">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/70">Last Contact</span>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Activity className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-tight">
|
||||||
|
{formatDateLastContact(agent.lastContact)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="scale-110">
|
||||||
|
<ConnectionCircle date={agent.lastContact}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronRight className="w-5 h-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Server} from "lucide-react";
|
|||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
import {AgentDatabaseCard} from "@/components/wrappers/dashboard/agent/agent-database-card";
|
||||||
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
import {AgentWithDatabases} from "@/db/schema/08_agent";
|
||||||
import {eventUpdate} from "@/types/events";
|
import {eventUpdate} from "@/types/events";
|
||||||
import {useAutoRefresh} from "@/hooks/use-auto-refresh";
|
import {useAutoRefresh} from "@/hooks/use-auto-refresh";
|
||||||
@@ -72,7 +72,7 @@ export const AgentContentPage = ({edgeKey, agent}: AgentContentPageProps) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} data={agent.databases}
|
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} data={agent.databases}
|
||||||
cardItem={DatabaseCard}/>
|
cardItem={AgentDatabaseCard}/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {Database} from "@/db/schema/07_database";
|
||||||
|
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||||
|
|
||||||
|
export type agentDatabaseCardProps = {
|
||||||
|
data: Database;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => {
|
||||||
|
const {data: database} = props;
|
||||||
|
|
||||||
|
return <DatabaseCard withDetails={false} data={database}/>;
|
||||||
|
|
||||||
|
//Todo: Add again when impersonation system will be implemented
|
||||||
|
/* if (!database.projectId) {
|
||||||
|
return <DatabaseCard data={database}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||||
|
href={`/dashboard/projects/${database.projectId}/database/${database.id}`}>
|
||||||
|
<DatabaseCard data={database}/>
|
||||||
|
</Link>
|
||||||
|
);*/
|
||||||
|
};
|
||||||
@@ -47,7 +47,6 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
|
|
||||||
const data = createAgent?.data?.data;
|
const data = createAgent?.data?.data;
|
||||||
if (createAgent?.serverError || !data) {
|
if (createAgent?.serverError || !data) {
|
||||||
console.log(createAgent?.serverError);
|
|
||||||
toast.error(createAgent?.serverError);
|
toast.error(createAgent?.serverError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,54 +57,53 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Card>
|
<Form
|
||||||
<CardContent>
|
form={form}
|
||||||
<Form
|
className="flex flex-col gap-4"
|
||||||
form={form}
|
onSubmit={async (values) => {
|
||||||
className="flex flex-col gap-4 mt-3"
|
await mutation.mutateAsync(values);
|
||||||
onSubmit={async (values) => {
|
}}
|
||||||
await mutation.mutateAsync(values);
|
>
|
||||||
}}
|
<FormField
|
||||||
>
|
control={form.control}
|
||||||
<FormField
|
name="name"
|
||||||
control={form.control}
|
defaultValue=""
|
||||||
name="name"
|
render={({field}) => (
|
||||||
defaultValue=""
|
<FormItem>
|
||||||
render={({field}) => (
|
<FormLabel>Name</FormLabel>
|
||||||
<FormItem>
|
<FormControl>
|
||||||
<FormLabel>Name</FormLabel>
|
<Input placeholder="Agent 1" {...field} />
|
||||||
<FormControl>
|
</FormControl>
|
||||||
<Input placeholder="Agent 1" {...field} />
|
<FormDescription>Your agent project name</FormDescription>
|
||||||
</FormControl>
|
<FormMessage/>
|
||||||
<FormDescription>Your agent project name</FormDescription>
|
</FormItem>
|
||||||
<FormMessage/>
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
<FormField
|
||||||
/>
|
control={form.control}
|
||||||
<FormField
|
defaultValue=""
|
||||||
control={form.control}
|
name="description"
|
||||||
defaultValue=""
|
render={({field}) => (
|
||||||
name="description"
|
<FormItem>
|
||||||
render={({field}) => (
|
<FormLabel>Description</FormLabel>
|
||||||
<FormItem>
|
<FormControl>
|
||||||
<FormLabel>Description</FormLabel>
|
<Input placeholder="This agent is for the client exemple.com" {...field}
|
||||||
<FormControl>
|
value={field.value ?? ""}/>
|
||||||
<Input placeholder="This agent is for the client exemple.com" {...field}
|
</FormControl>
|
||||||
value={field.value ?? ""}/>
|
<FormDescription>Enter your project agent description</FormDescription>
|
||||||
</FormControl>
|
<FormMessage/>
|
||||||
<FormDescription>Enter your project agent description</FormDescription>
|
</FormItem>
|
||||||
<FormMessage/>
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
<div className="flex justify-end">
|
||||||
/>
|
<Button type="submit">
|
||||||
<Button>{isCreate ? `Create agent` : `Save agent`}</Button>
|
{isCreate ? "Create" : "Update"}
|
||||||
</Form>
|
</Button>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</Form>
|
||||||
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useMutation} from "@tanstack/react-query";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action";
|
import {deleteAgentAction} from "@/components/wrappers/dashboard/agent/button-delete-agent/delete-agent.action";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type ButtonDeleteAgentProps = {
|
export type ButtonDeleteAgentProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -14,6 +15,7 @@ export type ButtonDeleteAgentProps = {
|
|||||||
|
|
||||||
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () => deleteAgentAction(props.agentId),
|
mutationFn: () => deleteAgentAction(props.agentId),
|
||||||
@@ -33,7 +35,7 @@ export const ButtonDeleteAgent = (props: ButtonDeleteAgentProps) => {
|
|||||||
description="Are you sure you want to remove this agent? This action cannot be undone."
|
description="Are you sure you want to remove this agent? This action cannot be undone."
|
||||||
button={{
|
button={{
|
||||||
main: {
|
main: {
|
||||||
text: props.text ? props.text : "",
|
text: props.text ? !isMobile ? props.text: "" : "",
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
icon: <Trash2 color="red"/>,
|
icon: <Trash2 color="red"/>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
|
import {EditNotifierButton} from "@/components/wrappers/dashboard/common/notifier/notifier-card/button-edit-notifier";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||||
|
import {truncateWords} from "@/utils/text";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type NotifierCardProps = {
|
export type NotifierCardProps = {
|
||||||
data: NotificationChannelWith;
|
data: NotificationChannelWith;
|
||||||
@@ -19,6 +21,7 @@ export type NotifierCardProps = {
|
|||||||
|
|
||||||
export const NotifierCard = (props: NotifierCardProps) => {
|
export const NotifierCard = (props: NotifierCardProps) => {
|
||||||
const {data, organization} = props;
|
const {data, organization} = props;
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block transition-all duration-200 rounded-xl">
|
<div className="block transition-all duration-200 rounded-xl">
|
||||||
@@ -33,13 +36,14 @@ export const NotifierCard = (props: NotifierCardProps) => {
|
|||||||
|
|
||||||
<div className="flex justify-start w-full">
|
<div className="flex justify-start w-full">
|
||||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
||||||
<h3 className="font-medium text-foreground">{data.name}</h3>
|
<h3 className="font-medium text-foreground">{isMobile ? truncateWords(data.name, 2) : data.name}</h3>
|
||||||
<Badge variant="secondary" className="text-xs font-mono">
|
<Badge variant="secondary" className="text-xs font-mono">
|
||||||
{data.provider}
|
{data.provider}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<EditNotifierButton
|
<EditNotifierButton
|
||||||
organizations={props.organizations}
|
organizations={props.organizations}
|
||||||
|
|||||||
+2
-2
@@ -7,11 +7,11 @@ export const NotificationChannelFormSchema = z.object({
|
|||||||
.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"),
|
||||||
|
|
||||||
provider: z.enum(["slack", "smtp"], {
|
provider: z.enum(["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"], {
|
||||||
required_error: "Provider is required",
|
required_error: "Provider is required",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
config: z.record(z.string()).optional(),
|
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),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {Input} from "@/components/ui/input";
|
|||||||
import {
|
import {
|
||||||
NotificationChannelFormSchema, NotificationChannelFormType
|
NotificationChannelFormSchema, NotificationChannelFormType
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.schema";
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
addNotificationChannelAction, updateNotificationChannelAction
|
addNotificationChannelAction, updateNotificationChannelAction
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-form.action";
|
||||||
@@ -20,12 +19,31 @@ import {
|
|||||||
import {
|
import {
|
||||||
NotifierSlackForm
|
NotifierSlackForm
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-slack.form";
|
||||||
|
import {
|
||||||
|
NotifierDiscordForm
|
||||||
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-discord.form";
|
||||||
|
import {
|
||||||
|
NotifierTelegramForm
|
||||||
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-telegram.form";
|
||||||
|
import {
|
||||||
|
NotifierGotifyForm
|
||||||
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-gotify.form";
|
||||||
|
import {
|
||||||
|
NotifierNtfyForm
|
||||||
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-ntfy.form";
|
||||||
|
import {
|
||||||
|
NotifierWebhookForm
|
||||||
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/providers/notifier-webhook.form";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
import {
|
import {
|
||||||
NotifierTestChannelButton
|
NotifierTestChannelButton
|
||||||
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
} from "@/components/wrappers/dashboard/common/notifier/notifier-form/notifier-test-channel-button";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
|
import {notificationTypes} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {Card} from "@/components/ui/card";
|
||||||
|
import {ArrowLeft} from "lucide-react";
|
||||||
|
|
||||||
type NotifierFormProps = {
|
type NotifierFormProps = {
|
||||||
onSuccessAction?: () => void;
|
onSuccessAction?: () => void;
|
||||||
@@ -34,7 +52,7 @@ type NotifierFormProps = {
|
|||||||
adminView?: boolean
|
adminView?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotifierForm = ({onSuccessAction, organization, defaultValues, adminView}: NotifierFormProps) => {
|
export const NotifierForm = ({onSuccessAction, organization, defaultValues}: NotifierFormProps) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isCreate = !Boolean(defaultValues);
|
const isCreate = !Boolean(defaultValues);
|
||||||
@@ -75,10 +93,36 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
|||||||
});
|
});
|
||||||
|
|
||||||
const provider = form.watch("provider");
|
const provider = form.watch("provider");
|
||||||
|
const selectedProviderDetails = notificationTypes.find(t => t.value === provider);
|
||||||
|
|
||||||
|
if (isCreate && !provider) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 py-4">
|
||||||
|
{notificationTypes.map((type) => {
|
||||||
|
const Icon = type.icon;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={type.value}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center gap-3 p-4 cursor-pointer hover:bg-accent/50 hover:border-primary/50 transition-all",
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("provider", type.value as any);
|
||||||
|
form.setValue("config", {});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-10 w-10 bg-secondary rounded-full flex items-center justify-center">
|
||||||
|
<Icon className="h-6 w-6 text-foreground"/>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-sm">{type.label}</span>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
@@ -86,6 +130,32 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
|||||||
await mutationAddNotificationChannel.mutateAsync(values);
|
await mutationAddNotificationChannel.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-2 p-3 bg-secondary/30 rounded-lg border border-border">
|
||||||
|
{selectedProviderDetails && (
|
||||||
|
<div className="h-10 w-10 bg-background rounded-full flex items-center justify-center border border-border shadow-sm">
|
||||||
|
<selectedProviderDetails.icon className="h-5 w-5"/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium">Configuring {selectedProviderDetails?.label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{isCreate ? "New Channel" : "Edit Channel"}</p>
|
||||||
|
</div>
|
||||||
|
{isCreate && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("provider", undefined as any);
|
||||||
|
form.setValue("config", undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2"/>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -93,7 +163,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Channel Name</FormLabel>
|
<FormLabel>Channel Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="e.g., Primary email, Team Slack" value={field.value ?? ""}/>
|
<Input {...field} placeholder={`My ${selectedProviderDetails?.label} Channel`} value={field.value ?? ""}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -104,28 +174,7 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="provider"
|
name="provider"
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<input type="hidden" {...field} value={field.value || ""} />
|
||||||
<FormLabel>Provider</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue("config", {});
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
value={field.value || ""}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select provider"/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="smtp">SMTP (Email)</SelectItem>
|
|
||||||
<SelectItem value="slack">Slack</SelectItem>
|
|
||||||
{/*<SelectItem value="curl">Curl</SelectItem>*/}
|
|
||||||
{/*<SelectItem value="webhook">Webhook</SelectItem>*/}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -137,8 +186,28 @@ export const NotifierForm = ({onSuccessAction, organization, defaultValues, admi
|
|||||||
<NotifierSlackForm form={form}/>
|
<NotifierSlackForm form={form}/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{provider === "discord" && (
|
||||||
|
<NotifierDiscordForm form={form}/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between">
|
{provider === "telegram" && (
|
||||||
|
<NotifierTelegramForm form={form}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{provider === "gotify" && (
|
||||||
|
<NotifierGotifyForm form={form}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{provider === "ntfy" && (
|
||||||
|
<NotifierNtfyForm form={form}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{provider === "webhook" && (
|
||||||
|
<NotifierWebhookForm form={form}/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
{defaultValues && (
|
{defaultValues && (
|
||||||
<NotifierTestChannelButton organizationId={organization?.id}
|
<NotifierTestChannelButton organizationId={organization?.id}
|
||||||
|
|||||||
-9
@@ -20,15 +20,6 @@ export const NotifierTestChannelButton = ({notificationChannel, organizationId}:
|
|||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
|
||||||
// const payload: EventPayload = {
|
|
||||||
// title: 'Database Down',
|
|
||||||
// message: 'Primary DB instance is unreachable',
|
|
||||||
// level: 'critical',
|
|
||||||
// data: {host: 'db-prod-01', error: 'connection timeout'},
|
|
||||||
// };
|
|
||||||
|
|
||||||
console.log("organizationId ici", organizationId);
|
|
||||||
|
|
||||||
const payload: EventPayload = {
|
const payload: EventPayload = {
|
||||||
title: 'Test Channel',
|
title: 'Test Channel',
|
||||||
message: `We are testing channel ${notificationChannel.name}`,
|
message: `We are testing channel ${notificationChannel.name}`,
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierDiscordFormProps = {
|
||||||
|
form: UseFormReturn<any, any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotifierDiscordForm = ({form}: NotifierDiscordFormProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.discordWebhook"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Discord Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://discord.com/api/webhooks/..."/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierGotifyFormProps = {
|
||||||
|
form: UseFormReturn<any, any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotifierGotifyForm = ({form}: NotifierGotifyFormProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.gotifyServerUrl"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gotify Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://gotify.example.com"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.gotifyAppToken"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Application Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierNtfyFormProps = {
|
||||||
|
form: UseFormReturn<any, any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.ntfyTopic"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Topic Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="my-secret-topic"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.ntfyServerUrl"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server URL (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://ntfy.sh (default)"/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-xs text-muted-foreground">Leave empty to use the official ntfy.sh server.</p>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.ntfyToken"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Access Token (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="tk_..."/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierTelegramFormProps = {
|
||||||
|
form: UseFormReturn<any, any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.telegramBotToken"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.telegramChatId"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="-100123456789 or 123456789"/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
You must start the conversation with the bot first (<strong>/start</strong>). <br/>
|
||||||
|
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>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import {UseFormReturn} from "react-hook-form";
|
||||||
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
|
type NotifierWebhookFormProps = {
|
||||||
|
form: UseFormReturn<any, any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator className="my-1"/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.webhookUrl"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://example.com/api/webhook"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.webhookSecretHeader"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Header Name (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="X-Webhook-Secret"/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.webhookSecret"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Secret Value (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Secret value..."/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>).
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Form, FormControl, FormField, FormItem, FormMessage, useZodForm} from "@/components/ui/form";
|
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||||
import {InfoIcon, Plus, Trash2} from "lucide-react";
|
import {InfoIcon, Plus, Trash2} from "lucide-react";
|
||||||
import {useFieldArray} from "react-hook-form";
|
import {useFieldArray} from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,6 @@ import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
|||||||
import {Label} from "@/components/ui/label";
|
import {Label} from "@/components/ui/label";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
@@ -22,7 +21,10 @@ import {
|
|||||||
} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action";
|
} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy.action";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
import {getNotificationChannelIcon} from "@/components/wrappers/dashboard/admin/notifications/helpers";
|
||||||
|
import {Card} from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
type AlertPolicyFormProps = {
|
type AlertPolicyFormProps = {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@@ -33,12 +35,12 @@ type AlertPolicyFormProps = {
|
|||||||
|
|
||||||
export const AlertPolicyForm = ({database, notificationChannels, organizationId, onSuccess}: AlertPolicyFormProps) => {
|
export const AlertPolicyForm = ({database, notificationChannels, organizationId, onSuccess}: AlertPolicyFormProps) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
|
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => {
|
const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => {
|
||||||
return alertPolicies.filter((alertPolicy)=> organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
|
return alertPolicies.filter((alertPolicy) => organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
|
||||||
notificationChannelId: alertPolicy.notificationChannelId,
|
notificationChannelId: alertPolicy.notificationChannelId,
|
||||||
eventKinds: alertPolicy.eventKinds,
|
eventKinds: alertPolicy.eventKinds,
|
||||||
enabled: alertPolicy.enabled
|
enabled: alertPolicy.enabled
|
||||||
@@ -48,11 +50,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: AlertPoliciesSchema,
|
schema: AlertPoliciesSchema,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [{
|
alertPolicies: database.alertPolicies && database.alertPolicies.length > 0 ? formattedAlertPoliciesList(database.alertPolicies) : [],
|
||||||
notificationChannelId: "",
|
|
||||||
eventKinds: [],
|
|
||||||
enabled: true,
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +61,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
|
|
||||||
|
|
||||||
const addAlertPolicy = () => {
|
const addAlertPolicy = () => {
|
||||||
append({id: "", eventKinds: [], enabled: true});
|
append({notificationChannelId: "", eventKinds: [], enabled: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAlertPolicy = (index: number) => {
|
const removeAlertPolicy = (index: number) => {
|
||||||
@@ -78,12 +76,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async ({alertPolicies}: AlertPoliciesType) => {
|
mutationFn: async ({alertPolicies}: AlertPoliciesType) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log(alertPolicies)
|
|
||||||
|
|
||||||
const defaultFormatedAlertPolicies = formattedAlertPoliciesList(database?.alertPolicies ?? []);
|
const defaultFormatedAlertPolicies = formattedAlertPoliciesList(database?.alertPolicies ?? []);
|
||||||
|
|
||||||
const alertPoliciesToAdd = alertPolicies?.filter(
|
const alertPoliciesToAdd = alertPolicies?.filter(
|
||||||
@@ -97,15 +89,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
const alertPoliciesToUpdate = alertPolicies?.filter((alertPolicy) => {
|
const alertPoliciesToUpdate = alertPolicies?.filter((alertPolicy) => {
|
||||||
const existing = defaultFormatedAlertPolicies.find((a) => a.notificationChannelId === alertPolicy.notificationChannelId);
|
const existing = defaultFormatedAlertPolicies.find((a) => a.notificationChannelId === alertPolicy.notificationChannelId);
|
||||||
return existing &&
|
return existing &&
|
||||||
(existing.eventKinds !== alertPolicy.eventKinds);
|
(existing.eventKinds !== alertPolicy.eventKinds || existing.enabled !== alertPolicy.enabled);
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
|
|
||||||
console.log("alertPoliciesToAdd", alertPoliciesToAdd)
|
|
||||||
console.log("alertPoliciesToRemove", alertPoliciesToRemove)
|
|
||||||
console.log("alertPoliciesToUpdate", alertPoliciesToUpdate)
|
|
||||||
|
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
alertPoliciesToAdd.length > 0
|
alertPoliciesToAdd.length > 0
|
||||||
? createAlertPoliciesAction({
|
? createAlertPoliciesAction({
|
||||||
@@ -129,8 +116,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(results);
|
|
||||||
|
|
||||||
const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
const rejected = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
||||||
if (rejected) {
|
if (rejected) {
|
||||||
throw new Error(rejected.reason?.message || "Network or server error");
|
throw new Error(rejected.reason?.message || "Network or server error");
|
||||||
@@ -144,7 +129,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
console.log("failedActions", failedActions);
|
|
||||||
if (failedActions.length > 0) {
|
if (failedActions.length > 0) {
|
||||||
const firstError = failedActions[0].data.actionError;
|
const firstError = failedActions[0].data.actionError;
|
||||||
const message = firstError?.message || "One or more operations failed";
|
const message = firstError?.message || "One or more operations failed";
|
||||||
@@ -155,7 +139,7 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Alert policies saved successfully");
|
toast.success("Alert policies saved successfully");
|
||||||
// onSuccess?.();
|
//onSuccess?.();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
@@ -167,158 +151,203 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
|||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-6"
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
await mutation.mutateAsync(values);
|
await mutation.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-sm font-medium">Alert Policies</Label>
|
<div>
|
||||||
|
<Label className="text-base font-medium">Configure Alerts</Label>
|
||||||
|
{!isMobile && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Choose which channels receive notifications for specific events.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
fields.length >= notificationChannels.length ||
|
fields.length >= notificationChannels.length ||
|
||||||
notificationChannels.length === 0
|
notificationChannels.length === 0
|
||||||
}
|
}
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline" size="sm" onClick={addAlertPolicy}>
|
size="sm"
|
||||||
<Plus className="w-4 h-4 mr-2"/>
|
className="h-8"
|
||||||
Add
|
onClick={addAlertPolicy}>
|
||||||
|
<Plus className="w-4 h-4 mr-1.5"/>
|
||||||
|
Add Policy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 w-full">
|
<div className="space-y-3 w-full">
|
||||||
{organizationNotificationChannels.length === 0 ? (
|
{organizationNotificationChannels.length === 0 ? (
|
||||||
<div className="text-muted-foreground text-sm text-center border border-dashed rounded-lg p-4">
|
<div
|
||||||
No alerts policy in organization
|
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||||
|
<InfoIcon className="h-8 w-8 text-muted-foreground/50"/>
|
||||||
|
<p className="font-medium text-sm text-foreground">No notification channels</p>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-xs">
|
||||||
|
Please <Link href={`/dashboard/settings`}
|
||||||
|
className="underline underline-offset-4 hover:text-primary transition-colors">configure
|
||||||
|
notification channels</Link> in your organization settings first.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : fields.length === 0 ? (
|
) : fields.length === 0 ? (
|
||||||
<div className="text-muted-foreground text-sm text-center">
|
<div
|
||||||
No alert policy, create one
|
className="flex flex-col items-center justify-center p-8 border border-dashed rounded-xl bg-muted/20 text-center gap-2">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<Plus className="h-4 w-4 text-primary"/>
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-sm text-foreground">No alert policies</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Click "Add Policy" to start receiving notifications.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => (
|
<div className="grid gap-4">
|
||||||
<div key={field.id}>
|
{fields.map((field, index) => (
|
||||||
<div className="flex w-full gap-3">
|
<Card key={field.id} className="p-4 transition-all hover:border-primary/50 relative group min-w-0 overflow-hidden">
|
||||||
<div className="flex-1">
|
<div className="flex flex-col gap-4">
|
||||||
<FormField
|
<div className="flex flex-row gap-2 items-start md:items-end flex-nowrap min-w-0 ">
|
||||||
control={form.control}
|
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
||||||
name={`alertPolicies.${index}.notificationChannelId`}
|
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||||
render={({field}) => {
|
Notification Channel
|
||||||
const selectedIds = form
|
</Label>
|
||||||
.watch("alertPolicies")
|
<FormField
|
||||||
.map((a: AlertPolicyType) => a.notificationChannelId)
|
control={form.control}
|
||||||
.filter(Boolean);
|
name={`alertPolicies.${index}.notificationChannelId`}
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedIds = form
|
||||||
|
.watch("alertPolicies")
|
||||||
|
.map((a: AlertPolicyType) => a.notificationChannelId)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const availableNotificationChannels = notificationChannels.filter(
|
const availableNotificationChannels = notificationChannels.filter(
|
||||||
(channel) =>
|
(channel) =>
|
||||||
channel.id.toString() === field.value?.toString() ||
|
channel.id.toString() === field.value?.toString() ||
|
||||||
!selectedIds.includes(channel.id.toString())
|
!selectedIds.includes(channel.id.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const selectedChannel = notificationChannels.find(c => c.id === field.value);
|
||||||
<FormItem>
|
|
||||||
<Select onValueChange={field.onChange}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem className="space-y-0 min-w-0">
|
||||||
|
<Select onValueChange={field.onChange} value={field.value?.toString() || ""}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="h-9 w-full bg-background border-input min-w-0">
|
||||||
|
<SelectValue placeholder="Select channel">
|
||||||
|
{selectedChannel && (
|
||||||
|
<div className="flex items-center gap-2 min-w-0 w-full">
|
||||||
|
<div className="flex items-center justify-center h-4 w-4 shrink-0">
|
||||||
|
{getNotificationChannelIcon(selectedChannel.provider)}
|
||||||
|
</div>
|
||||||
|
<span className="truncate font-medium text-sm min-w-0">
|
||||||
|
{selectedChannel.name}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-[9px] bg-secondary px-1.5 py-0.5 rounded text-muted-foreground font-mono uppercase">
|
||||||
|
{selectedChannel.provider}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{availableNotificationChannels.map(channel => (
|
||||||
|
<SelectItem key={channel.id.toString()} value={channel.id.toString()}>
|
||||||
|
<div className="flex items-center gap-2 w-full min-w-0">
|
||||||
|
<div className="text-muted-foreground scale-90 shrink-0">
|
||||||
|
{getNotificationChannelIcon(channel.provider)}
|
||||||
|
</div>
|
||||||
|
<span className="font-medium truncate min-w-0">{channel.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground ml-2 capitalize shrink-0">
|
||||||
|
({channel.provider})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage className="mt-1"/>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5 shrink-0">
|
||||||
|
<Label className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest pl-0.5">
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`alertPolicies.${index}.enabled`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="h-10 w-full ">
|
<div className="flex items-center h-9 px-1 md:px-3 rounded-md border border-input bg-background justify-between min-w-0">
|
||||||
<SelectValue
|
{!isMobile && (
|
||||||
placeholder="Select notification channel"/>
|
<Label htmlFor={`switch-${index}`} className="text-xs cursor-pointer font-medium text-foreground mr-2">
|
||||||
</SelectTrigger>
|
{field.value ? "Active" : "Off"}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} id={`switch-${index}`} className="scale-75 origin-right"/>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
</FormItem>
|
||||||
{availableNotificationChannels.map((channel) => (
|
)}
|
||||||
<SelectItem key={channel.id.toString()}
|
/>
|
||||||
value={channel.id.toString()}>
|
</div>
|
||||||
{channel.name}
|
<div className="flex flex-col gap-1.5 shrink-0 mt-auto">
|
||||||
</SelectItem>
|
<Button
|
||||||
))}
|
type="button"
|
||||||
</SelectContent>
|
variant="outline"
|
||||||
</Select>
|
size="icon"
|
||||||
<FormMessage className="mb-2"/>
|
className="h-9 w-9 text-muted-foreground hover:text-destructive hover:border-destructive/50 hover:bg-destructive/10 transition-colors border-input bg-background"
|
||||||
</FormItem>
|
onClick={() => removeAlertPolicy(index)}
|
||||||
);
|
>
|
||||||
}}
|
<Trash2 className="w-4 h-4"/>
|
||||||
/>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`alertPolicies.${index}.eventKinds`}
|
name={`alertPolicies.${index}.eventKinds`}
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="space-y-1.5 min-w-0">
|
||||||
|
<FormLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Trigger Events
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MultiSelect
|
<div className="max-w-full overflow-hidden">
|
||||||
options={EVENT_KIND_OPTIONS}
|
<MultiSelect
|
||||||
onValueChange={field.onChange}
|
options={EVENT_KIND_OPTIONS}
|
||||||
defaultValue={field.value ?? []}
|
onValueChange={field.onChange}
|
||||||
placeholder="Select event kinds"
|
defaultValue={field.value ?? []}
|
||||||
variant="inverted"
|
placeholder={isMobile ? "Select events...": "Select events to trigger notifications..."}
|
||||||
animation={0}
|
variant="inverted"
|
||||||
/>
|
animation={0}
|
||||||
|
className="bg-background/50 w-full min-w-0 flex-wrap"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
<div className="flex flex-col gap-3 justify-between items-center ">
|
))}
|
||||||
<Button type="button" variant="outline"
|
</div>
|
||||||
onClick={() => removeAlertPolicy(index)}>
|
|
||||||
<Trash2 className="w-4 h-4"/>
|
|
||||||
</Button>
|
|
||||||
<div className="flex h-full justify-center items-center">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div tabIndex={0} className="inline-flex rounded-md">
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`alertPolicies.${index}.enabled`}
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-64 text-pretty">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<InfoIcon className="size-4"/>
|
|
||||||
<p>This is for activating the alert policy</p>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{index + 1 < fields.length && <Separator className="mt-3"/>}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 justify-end">
|
<div className="flex gap-3 justify-end pt-2 border-t mt-2">
|
||||||
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>
|
<ButtonWithLoading variant="outline" type="button" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
<ButtonWithLoading isPending={false}>
|
<ButtonWithLoading isPending={mutation.isPending}>
|
||||||
Save
|
Save Changes
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const uploadBackupAction = userAction
|
|||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
const fileSize = file.size;
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const fileName = `imported_${uuid}${fileExtension}`;
|
const fileName = `imported_${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
@@ -85,6 +86,7 @@ export const uploadBackupAction = userAction
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
databaseId: database.id,
|
databaseId: database.id,
|
||||||
file: fileName,
|
file: fileName,
|
||||||
|
fileSize: fileSize,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export function CreateOrganizationModal({
|
|||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: OrganizationSchema) => {
|
mutationFn: async (values: OrganizationSchema) => {
|
||||||
console.log(values);
|
|
||||||
|
|
||||||
const result = await createOrganizationAction(values);
|
const result = await createOrganizationAction(values);
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ export function CreateOrganizationModal({
|
|||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
console.log(values);
|
|
||||||
await mutation.mutateAsync(values);
|
await mutation.mutateAsync(values);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ import {useRouter} from "next/navigation";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
import {Trash2} from "lucide-react";
|
import {Trash2} from "lucide-react";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type DeleteOrganizationButtonProps = {
|
export type DeleteOrganizationButtonProps = {
|
||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
@@ -13,6 +14,7 @@ export type DeleteOrganizationButtonProps = {
|
|||||||
|
|
||||||
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
|
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -47,7 +49,7 @@ export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) =
|
|||||||
description="Are you sure you want to remove this organization? This action cannot be undone."
|
description="Are you sure you want to remove this organization? This action cannot be undone."
|
||||||
button={{
|
button={{
|
||||||
main: {
|
main: {
|
||||||
text: "Delete Organization",
|
text: !isMobile ? "Delete Organization" : "",
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
icon: <Trash2 color="red"/>,
|
icon: <Trash2 color="red"/>,
|
||||||
},
|
},
|
||||||
|
|||||||
+70
-73
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
@@ -22,7 +21,6 @@ import {
|
|||||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {
|
import {
|
||||||
deleteOrganizationAction,
|
|
||||||
updateOrganizationAction
|
updateOrganizationAction
|
||||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
@@ -95,76 +93,75 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Form
|
||||||
<CardHeader></CardHeader>
|
form={form}
|
||||||
<CardContent>
|
className="flex flex-col gap-4"
|
||||||
<Form
|
onSubmit={async (values) => {
|
||||||
form={form}
|
await mutation.mutateAsync(values);
|
||||||
className="flex flex-col gap-4"
|
}}
|
||||||
onSubmit={async (values) => {
|
>
|
||||||
await mutation.mutateAsync(values);
|
<FormField
|
||||||
}}
|
control={form.control}
|
||||||
>
|
name="name"
|
||||||
<FormField
|
defaultValue=""
|
||||||
control={form.control}
|
render={({field}) => (
|
||||||
name="name"
|
<FormItem>
|
||||||
defaultValue=""
|
<FormLabel>Name</FormLabel>
|
||||||
render={({field}) => (
|
<FormControl>
|
||||||
<FormItem>
|
<Input placeholder="Organization 1" {...field} />
|
||||||
<FormLabel>Name</FormLabel>
|
</FormControl>
|
||||||
<FormControl>
|
<FormMessage/>
|
||||||
<Input placeholder="Organization 1" {...field} />
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<FormMessage/>
|
/>
|
||||||
</FormItem>
|
<FormField
|
||||||
)}
|
control={form.control}
|
||||||
/>
|
name="slug"
|
||||||
<FormField
|
defaultValue=""
|
||||||
control={form.control}
|
render={({field}) => (
|
||||||
name="slug"
|
<FormItem>
|
||||||
defaultValue=""
|
<FormLabel>Slug</FormLabel>
|
||||||
render={({field}) => (
|
<FormControl>
|
||||||
<FormItem>
|
<Input
|
||||||
<FormLabel>Slug</FormLabel>
|
placeholder="project-1"
|
||||||
<FormControl>
|
{...field}
|
||||||
<Input
|
onChange={(e) => {
|
||||||
placeholder="project-1"
|
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
||||||
{...field}
|
field.onChange(value);
|
||||||
onChange={(e) => {
|
}}
|
||||||
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
|
/>
|
||||||
field.onChange(value);
|
</FormControl>
|
||||||
}}
|
<FormMessage/>
|
||||||
/>
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<FormMessage/>
|
/>
|
||||||
</FormItem>
|
<FormField
|
||||||
)}
|
control={form.control}
|
||||||
/>
|
name="users"
|
||||||
<FormField
|
render={({field}) => (
|
||||||
control={form.control}
|
<FormItem>
|
||||||
name="users"
|
<FormLabel>Users</FormLabel>
|
||||||
render={({field}) => (
|
<FormControl>
|
||||||
<FormItem>
|
<MultiSelect
|
||||||
<FormLabel>Users</FormLabel>
|
options={formatUsersList(props.users)}
|
||||||
<FormControl>
|
onValueChange={field.onChange}
|
||||||
<MultiSelect
|
defaultValue={field.value ?? []}
|
||||||
options={formatUsersList(props.users)}
|
placeholder="Select users"
|
||||||
onValueChange={field.onChange}
|
variant="inverted"
|
||||||
defaultValue={field.value ?? []}
|
animation={2}
|
||||||
placeholder="Select users"
|
// maxCount={100}
|
||||||
variant="inverted"
|
/>
|
||||||
animation={2}
|
</FormControl>
|
||||||
// maxCount={100}
|
<FormDescription>Select users you want to add to this organization</FormDescription>
|
||||||
/>
|
<FormMessage/>
|
||||||
</FormControl>
|
</FormItem>
|
||||||
<FormDescription>Select users you want to add to this organization</FormDescription>
|
)}
|
||||||
<FormMessage/>
|
/>
|
||||||
</FormItem>
|
<div className="flex justify-end">
|
||||||
)}
|
<Button type="submit">
|
||||||
/>
|
{isCreate ? "Create" : "Update"}
|
||||||
<Button>{isCreate ? `Create Organization` : `Update Organization`}</Button>
|
</Button>
|
||||||
</Form>
|
</div>
|
||||||
</CardContent>
|
</Form>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
-4
@@ -28,10 +28,6 @@ export const OrganizationNotifiersTab = ({
|
|||||||
<h3 className="text-xl font-semibold text-balance mb-1">
|
<h3 className="text-xl font-semibold text-balance mb-1">
|
||||||
Notification Settings
|
Notification Settings
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground leading-relaxed ">
|
|
||||||
Configure how and when you receive alerts about your services and
|
|
||||||
infrastructure.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<NotifierAddEditModal
|
<NotifierAddEditModal
|
||||||
organization={organization}
|
organization={organization}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
||||||
import { UploadIcon } from "lucide-react";
|
import {UploadIcon} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import {toast} from "sonner";
|
||||||
import { uploadImageAction } from "@/features/upload/public/upload.action";
|
import {uploadImageAction} from "@/features/upload/public/upload.action";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/actions/avatar.action";
|
import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/actions/avatar.action";
|
||||||
import { useRouter } from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import { User } from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
import React, {ChangeEvent} from "react";
|
import React, {ChangeEvent} from "react";
|
||||||
|
|
||||||
export type AvatarWithUploadProps = {
|
export type AvatarWithUploadProps = {
|
||||||
@@ -25,7 +25,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
const data = uploadImage?.data?.data;
|
const data = uploadImage?.data?.data;
|
||||||
|
|
||||||
if (uploadImage?.serverError || !data) {
|
if (uploadImage?.serverError || !data) {
|
||||||
console.log(uploadImage?.serverError);
|
|
||||||
toast.error(uploadImage?.serverError);
|
toast.error(uploadImage?.serverError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,7 +33,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
const dataUser = updateUser?.data?.data;
|
const dataUser = updateUser?.data?.data;
|
||||||
|
|
||||||
if (updateUser?.serverError || !dataUser) {
|
if (updateUser?.serverError || !dataUser) {
|
||||||
console.log(updateUser?.serverError);
|
|
||||||
toast.error(updateUser?.serverError);
|
toast.error(updateUser?.serverError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -55,12 +53,11 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative ">
|
<div className="relative ">
|
||||||
|
|
||||||
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
|
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
|
||||||
<AvatarImage src={user.image || undefined}/>
|
<AvatarImage className="object-cover" src={user.image || undefined}/>
|
||||||
<AvatarFallback className="text-3xl">{user.name.charAt(0)}</AvatarFallback>
|
<AvatarFallback className="text-3xl">{user.name.charAt(0)}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
}}
|
}}
|
||||||
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
|
||||||
>
|
>
|
||||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary" />
|
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export function ProfileAccount({user}: ProfileAccountProps) {
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: (error: BetterAuthError) => {
|
onError: (error: BetterAuthError) => {
|
||||||
console.log(error)
|
|
||||||
if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") {
|
if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") {
|
||||||
toast.error("User already exists, use another email address!");
|
toast.error("User already exists, use another email address!");
|
||||||
emailForm.reset({email: user.email});
|
emailForm.reset({email: user.email});
|
||||||
|
|||||||
+4
-1
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/delete-project.action";
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/delete-project.action";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
import {useIsMobile} from "@/hooks/use-mobile";
|
||||||
|
|
||||||
export type ButtonDeleteProjectProps = {
|
export type ButtonDeleteProjectProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -16,6 +17,8 @@ export type ButtonDeleteProjectProps = {
|
|||||||
|
|
||||||
export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
|
export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () => deleteProjectAction(props.projectId),
|
mutationFn: () => deleteProjectAction(props.projectId),
|
||||||
onSuccess: async (result: any) => {
|
onSuccess: async (result: any) => {
|
||||||
@@ -35,7 +38,7 @@ export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
|
|||||||
description="Are you sure you want to delete this project ? This action cannot be undone."
|
description="Are you sure you want to delete this project ? This action cannot be undone."
|
||||||
button={{
|
button={{
|
||||||
main: {
|
main: {
|
||||||
text: props.text ? props.text : "",
|
text: props.text ? !isMobile ? props.text: "" : "",
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
icon: <Trash2 color="red"/>,
|
icon: <Trash2 color="red"/>,
|
||||||
},
|
},
|
||||||
|
|||||||
-2
@@ -21,8 +21,6 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
|||||||
|
|
||||||
|
|
||||||
const databasesToRemove = databasesUpdated.map((db) => db.id);
|
const databasesToRemove = databasesUpdated.map((db) => db.id);
|
||||||
console.log(databasesUpdated);
|
|
||||||
console.log(databasesToRemove);
|
|
||||||
|
|
||||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
|
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
console.log("Deleting rows:", rows)
|
|
||||||
await mutationDeleteBackups.mutateAsync(rows)
|
await mutationDeleteBackups.mutateAsync(rows)
|
||||||
}}
|
}}
|
||||||
className="text-red-600 focus:text-red-700"
|
className="text-red-600 focus:text-red-700"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {ProjectWith} from "@/db/schema/06_project";
|
import {ProjectWith} from "@/db/schema/06_project";
|
||||||
|
import {ChevronRight, Folder} from "lucide-react";
|
||||||
|
import {Badge} from "@/components/ui/badge";
|
||||||
|
|
||||||
export type projectCardProps = {
|
export type projectCardProps = {
|
||||||
data: ProjectWith;
|
data: ProjectWith;
|
||||||
@@ -10,19 +12,39 @@ export type projectCardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectCard = (props: projectCardProps) => {
|
export const ProjectCard = (props: projectCardProps) => {
|
||||||
const { data: project, organizationSlug } = props;
|
const { data: project } = props;
|
||||||
|
const dbCount = project.databases.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/projects/${project.id}`}
|
href={`/dashboard/projects/${project.id}`}
|
||||||
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||||
>
|
>
|
||||||
<Card className="flex flex-row justify-between">
|
<Card className="relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
|
||||||
<div className="flex-1 text-left">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<CardHeader className="text-2xl font-bold">
|
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-300 shadow-inner">
|
||||||
|
<Folder className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<Badge className="text-[10px] font-medium px-2 py-1 rounded-lg bg-secondary/50 text-foreground">{dbCount} {dbCount === 1 ? "Database" : "Databases"}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
|
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors line-clamp-1 tracking-tight">
|
||||||
{project.name}
|
{project.name}
|
||||||
</CardHeader>
|
</h3>
|
||||||
<CardContent>{project.databases.length} databases</CardContent>
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
||||||
|
Manage your databases and backup policies for this project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">View Project</span>
|
||||||
|
<div className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span>
|
||||||
|
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {useState} from "react";
|
||||||
|
import {Card} from "@/components/ui/card";
|
||||||
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
|
import {ChevronRight, Activity, Fingerprint, Copy, Check} from "lucide-react";
|
||||||
|
|
||||||
export type projectDatabaseCardProps = {
|
export type projectDatabaseCardProps = {
|
||||||
data: Database;
|
data: Database;
|
||||||
@@ -15,11 +16,12 @@ export type projectDatabaseCardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||||
const {organizationSlug, data: database, extendedProps: extendedProps} = props;
|
const {data: database, extendedProps: extendedProps} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
|
<Link
|
||||||
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
className="group block transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl"
|
||||||
|
href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
||||||
<DatabaseCard data={database}/>
|
<DatabaseCard data={database}/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -27,27 +29,89 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
|||||||
|
|
||||||
export type databaseCardProps = {
|
export type databaseCardProps = {
|
||||||
data: Database;
|
data: Database;
|
||||||
|
withDetails?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseCard = (props: databaseCardProps) => {
|
export const DatabaseCard = (props: databaseCardProps) => {
|
||||||
const {data: database} = props;
|
const {data: database, withDetails = true} = props;
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
navigator.clipboard.writeText(database.agentDatabaseId);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-row justify-between space-x-2 ">
|
<Card
|
||||||
<div className="flex items-center justify-start w-full space-x-5 ">
|
className="relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0">
|
||||||
<Image src={`/images/${database.dbms}.png`} alt="Database type Icon" width={60} height={60}
|
<div className="flex items-start justify-between mb-2">
|
||||||
className="object-cover ml-4"/>
|
<div
|
||||||
<div className="justify-between">
|
className="relative w-12 h-12 p-2 bg-background rounded-xl border border-border/50 shadow-sm flex items-center justify-center group-hover:border-primary/30 transition-all duration-300 group-hover:scale-105">
|
||||||
<div className="font-medium">Name: {database.name}</div>
|
<Image
|
||||||
<div className="text-sm text-muted-foreground">Generated Id: {database.agentDatabaseId}</div>
|
src={`/images/${database.dbms}.png`}
|
||||||
<div className="text-xs text-muted-foreground">
|
alt={`${database.dbms} icon`}
|
||||||
Last contact: {formatDateLastContact(database.lastContact)}
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="object-contain w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-3">
|
||||||
|
<div className="scale-100 origin-right">
|
||||||
|
<ConnectionCircle date={database.lastContact}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center px-5 justify-center">
|
|
||||||
<ConnectionCircle date={database.lastContact}/>
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
|
<h3 className="text-lg font-black text-foreground group-hover:text-primary transition-colors truncate tracking-tight">
|
||||||
|
{database.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<div className="p-1 bg-muted/50 rounded-lg">
|
||||||
|
<Fingerprint className="w-3.5 h-3.5"/>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-[10px] font-bold truncate">
|
||||||
|
{database.agentDatabaseId}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="ml-1 p-0.5 hover:bg-muted rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 z-10"
|
||||||
|
title="Copy ID"
|
||||||
|
>
|
||||||
|
{isCopied ? <Check className="w-2.5 h-2.5 text-green-500"/> :
|
||||||
|
<Copy className="w-2.5 h-2.5"/>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<div className="p-1 bg-muted/50 rounded-lg">
|
||||||
|
<Activity className="w-3.5 h-3.5"/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-tight">
|
||||||
|
{formatDateLastContact(database.lastContact)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{withDetails && (
|
||||||
|
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-black uppercase tracking-widest text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-[-10px] group-hover:translate-x-0">Open</span>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 text-muted-foreground group-hover:text-primary transition-colors">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-bold uppercase tracking-widest group-hover:hidden">Details</span>
|
||||||
|
<ChevronRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export const createProjectAction = userAction
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
|
|||||||
@@ -78,9 +78,6 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
|
||||||
<CardHeader></CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
@@ -124,9 +121,11 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button>{isCreate ? `Create Project` : `Update Project`}</Button>
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit">
|
||||||
|
{isCreate ? "Create" : "Update"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "backups" ADD COLUMN "file_size" integer;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TYPE "public"."provider_kind" ADD VALUE 'discord';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."provider_kind" ADD VALUE 'telegram';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TYPE "public"."provider_kind" ADD VALUE 'gotify';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."provider_kind" ADD VALUE 'ntfy';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."provider_kind" ADD VALUE 'webhook';
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,34 @@
|
|||||||
"when": 1766426190521,
|
"when": 1766426190521,
|
||||||
"tag": "0015_absurd_next_avengers",
|
"tag": "0015_absurd_next_avengers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767363779637,
|
||||||
|
"tag": "0016_broken_morgan_stark",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767778503470,
|
||||||
|
"tag": "0017_wild_purple_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767780819624,
|
||||||
|
"tag": "0018_smiling_mole_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1767782077775,
|
||||||
|
"tag": "0019_overjoyed_butterfly",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@ export const backup = pgTable(
|
|||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
status: statusEnum("status").default("waiting").notNull(),
|
status: statusEnum("status").default("waiting").notNull(),
|
||||||
file: text("file"),
|
file: text("file"),
|
||||||
|
fileSize: integer("file_size"),
|
||||||
databaseId: uuid("database_id")
|
databaseId: uuid("database_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => database.id, {onDelete: "cascade"}),
|
.references(() => database.id, {onDelete: "cascade"}),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {z} from "zod";
|
|||||||
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
import {OrganizationInvitation} from "@/db/schema/05_invitation";
|
||||||
|
|
||||||
|
|
||||||
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp']);
|
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook']);
|
||||||
|
|
||||||
export const notificationChannel = pgTable('notification_channel', {
|
export const notificationChannel = pgTable('notification_channel', {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {cn} from "@/lib/utils";
|
|||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||||
import {isImportedFilename} from "@/utils/text";
|
import {formatBytes, isImportedFilename} from "@/utils/text";
|
||||||
|
|
||||||
|
|
||||||
export function backupColumns(
|
export function backupColumns(
|
||||||
@@ -66,13 +66,18 @@ export function backupColumns(
|
|||||||
header: "Reference",
|
header: "Reference",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const fileName = row.original.file
|
const fileName = row.original.file
|
||||||
console.log(row.original)
|
|
||||||
const reference = row.original.id
|
const reference = row.original.id
|
||||||
const isImported = isImportedFilename(`${fileName}`)
|
const isImported = isImportedFilename(`${fileName}`)
|
||||||
console.log(isImported)
|
|
||||||
return isImported ? `${reference} (imported)` : `${reference}`
|
return isImported ? `${reference} (imported)` : `${reference}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "fileSize",
|
||||||
|
header: "Size",
|
||||||
|
cell: ({row}) => {
|
||||||
|
return formatBytes(row.getValue("fileSize"))
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: "Created At",
|
header: "Created At",
|
||||||
@@ -155,7 +160,6 @@ export function backupColumns(
|
|||||||
} else if (settings.storage == "s3") {
|
} else if (settings.storage == "s3") {
|
||||||
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
|
||||||
}
|
}
|
||||||
console.log(data)
|
|
||||||
if (data?.data?.success) {
|
if (data?.data?.success) {
|
||||||
url = data.data.value ?? "";
|
url = data.data.value ?? "";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||||
import {NextResponse} from "next/server";
|
|
||||||
import {
|
import {
|
||||||
deleteFileS3Private,
|
deleteFileS3Private,
|
||||||
deleteLocalPrivate,
|
deleteLocalPrivate,
|
||||||
uploadLocalPrivate,
|
|
||||||
uploadS3Private
|
|
||||||
} from "@/features/upload/private/upload.action";
|
} from "@/features/upload/private/upload.action";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
@@ -89,98 +86,32 @@ export const deleteBackupAction = userAction
|
|||||||
|
|
||||||
let success: boolean, message: string;
|
let success: boolean, message: string;
|
||||||
|
|
||||||
const result =
|
if (parsedInput.file) {
|
||||||
settings.storage === "local"
|
const result =
|
||||||
? await deleteLocalPrivate(parsedInput.file)
|
settings.storage === "local"
|
||||||
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
? await deleteLocalPrivate(parsedInput.file)
|
||||||
|
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
||||||
|
|
||||||
({success, message} = result);
|
({success, message} = result);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
actionError: {
|
actionError: {
|
||||||
message: message,
|
message: message,
|
||||||
status: 404,
|
status: 404,
|
||||||
cause: "Unable to delete backup from storage",
|
cause: "Unable to delete backup from storage",
|
||||||
messageParams: {message: "Error deleting the backup"},
|
messageParams: {message: "Error deleting the backup"},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
|
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
// if (!settings) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// actionError: {
|
|
||||||
// message: "No settings found.",
|
|
||||||
// status: 404,
|
|
||||||
// cause: "No settings found.",
|
|
||||||
// messageParams: {message: "Error deleting the backup"},
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// let success: boolean, message: string;
|
|
||||||
//
|
|
||||||
// const result =
|
|
||||||
// settings.storage === "local"
|
|
||||||
// ? await deleteLocalPrivate(parsedInput.file)
|
|
||||||
// : await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
|
||||||
//
|
|
||||||
// ({success, message} = result);
|
|
||||||
//
|
|
||||||
// if (!success) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// actionError: {
|
|
||||||
// message: message,
|
|
||||||
// status: 404,
|
|
||||||
// cause: "Unable to delete backup from storage",
|
|
||||||
// messageParams: {message: "Error deleting the backup"},
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await db
|
|
||||||
// .delete(drizzleDb.schemas.backup)
|
|
||||||
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
// const backupExists = await db
|
|
||||||
// .select()
|
|
||||||
// .from(drizzleDb.schemas.backup)
|
|
||||||
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if (backupExists.length === 0) {
|
|
||||||
// return {
|
|
||||||
// success: true,
|
|
||||||
// actionSuccess: {
|
|
||||||
// message: "Backup deleted successfully.",
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// } else {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// actionError: {
|
|
||||||
// message: "Backup not found or already deleted.",
|
|
||||||
// status: 404,
|
|
||||||
// cause: "Backup could not be deleted (from database or remote storage).",
|
|
||||||
// messageParams: {message: "Error deleting the backup"},
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -246,7 +177,6 @@ export const rerunRestorationAction = userAction
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Create Restoration Action (Drizzle version)
|
|
||||||
export const createRestorationAction = userAction
|
export const createRestorationAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -256,7 +186,6 @@ export const createRestorationAction = userAction
|
|||||||
)
|
)
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
||||||
try {
|
try {
|
||||||
// Insert new restoration into the database
|
|
||||||
const restorationData = await db
|
const restorationData = await db
|
||||||
.insert(drizzleDb.schemas.restoration)
|
.insert(drizzleDb.schemas.restoration)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {db} from "@/db";
|
|||||||
import {notificationLog} from "@/db/schema/11_notification-log";
|
import {notificationLog} from "@/db/schema/11_notification-log";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {Json} from "drizzle-zod";
|
import {Json} from "drizzle-zod";
|
||||||
import {AlertPolicy} from "@/db/schema/10_alert-policy";
|
|
||||||
|
|
||||||
export async function dispatchNotification(
|
export async function dispatchNotification(
|
||||||
payload: EventPayload,
|
payload: EventPayload,
|
||||||
@@ -85,7 +84,7 @@ export async function dispatchNotification(
|
|||||||
success: false,
|
success: false,
|
||||||
channelId: channelId || "",
|
channelId: channelId || "",
|
||||||
provider: null,
|
provider: null,
|
||||||
error: "Channel not active¬",
|
error: "Channel not active",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +1,64 @@
|
|||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
import { DatabaseWith } from "@/db/schema/07_database";
|
||||||
import {EventKind, EventPayload} from "@/features/notifications/types";
|
import { EventKind, EventPayload } from "@/features/notifications/types";
|
||||||
import {dispatchNotification} from "@/features/notifications/dispatch";
|
import { dispatchNotification } from "@/features/notifications/dispatch";
|
||||||
|
|
||||||
|
|
||||||
export async function sendNotificationsBackupRestore(database: DatabaseWith, event: EventKind) {
|
export async function sendNotificationsBackupRestore(database: DatabaseWith, event: EventKind) {
|
||||||
|
if (!database.alertPolicies || database.alertPolicies.length === 0) {
|
||||||
if (database.alertPolicies && database.alertPolicies.length > 0) {
|
return [];
|
||||||
for (const alertPolicy of database.alertPolicies) {
|
|
||||||
if (alertPolicy.enabled) {
|
|
||||||
|
|
||||||
const eventKinds = alertPolicy.eventKinds
|
|
||||||
|
|
||||||
if (eventKinds.includes(event)) {
|
|
||||||
|
|
||||||
const date = new Date()
|
|
||||||
|
|
||||||
let level: "info" | "critical" = "info";
|
|
||||||
let message = "";
|
|
||||||
let error: string | null = null;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "error_backup":
|
|
||||||
case "error_restore":
|
|
||||||
level = "critical";
|
|
||||||
message = `An error occurred during ${event.includes("backup") ? "backup" : "restore"} on ${date.toISOString()}.`;
|
|
||||||
error = "Check database connection or agent";
|
|
||||||
break;
|
|
||||||
case "success_backup":
|
|
||||||
case "success_restore":
|
|
||||||
level = "info";
|
|
||||||
message = `${event.includes("backup") ? "Backup" : "Restore"} completed successfully at ${date.toISOString()}.`;
|
|
||||||
break;
|
|
||||||
case "weekly_report":
|
|
||||||
level = "info";
|
|
||||||
message = `Weekly report generated at ${date.toISOString()}.`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleMap: Record<EventKind, string> = {
|
|
||||||
error_backup: `Backup Notification`,
|
|
||||||
error_restore: `Restore Notification`,
|
|
||||||
success_backup: `Backup Notification`,
|
|
||||||
success_restore: `Restore Notification`,
|
|
||||||
weekly_report: `Weekly Report Notification`,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const payload: EventPayload = {
|
|
||||||
title: titleMap[event],
|
|
||||||
message,
|
|
||||||
level: level,
|
|
||||||
event: event,
|
|
||||||
data: {
|
|
||||||
host: database.name,
|
|
||||||
id: database.id,
|
|
||||||
agentDatabaseId: database.agentDatabaseId,
|
|
||||||
error,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return await dispatchNotification(payload, alertPolicy.id, undefined, undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activePolicies = database.alertPolicies.filter(policy =>
|
||||||
|
policy.enabled && policy.eventKinds.includes(event)
|
||||||
|
);
|
||||||
|
|
||||||
|
const promises = activePolicies.map(alertPolicy => {
|
||||||
|
const date = new Date();
|
||||||
|
let level: "info" | "critical" = "info";
|
||||||
|
let message = "";
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case "error_backup":
|
||||||
|
case "error_restore":
|
||||||
|
level = "critical";
|
||||||
|
message = `An error occurred during ${event.includes("backup") ? "backup" : "restore"} on ${date.toISOString()}.`;
|
||||||
|
error = "Check database connection or agent";
|
||||||
|
break;
|
||||||
|
case "success_backup":
|
||||||
|
case "success_restore":
|
||||||
|
level = "info";
|
||||||
|
message = `${event.includes("backup") ? "Backup" : "Restore"} completed successfully at ${date.toISOString()}.`;
|
||||||
|
break;
|
||||||
|
case "weekly_report":
|
||||||
|
level = "info";
|
||||||
|
message = `Weekly report generated at ${date.toISOString()}.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleMap: Record<EventKind, string> = {
|
||||||
|
error_backup: `Backup Notification`,
|
||||||
|
error_restore: `Restore Notification`,
|
||||||
|
success_backup: `Backup Notification`,
|
||||||
|
success_restore: `Restore Notification`,
|
||||||
|
weekly_report: `Weekly Report Notification`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload: EventPayload = {
|
||||||
|
title: titleMap[event],
|
||||||
|
message,
|
||||||
|
level: level,
|
||||||
|
event: event,
|
||||||
|
data: {
|
||||||
|
host: database.name,
|
||||||
|
id: database.id,
|
||||||
|
agentDatabaseId: database.agentDatabaseId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatchNotification(payload, alertPolicy.id, undefined, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
export async function sendDiscord(
|
||||||
|
config: { discordWebhook: string },
|
||||||
|
payload: EventPayload
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
const {discordWebhook: webhookUrl} = config;
|
||||||
|
|
||||||
|
const embeds = [
|
||||||
|
{
|
||||||
|
title: `[${payload.level.toUpperCase()}] ${payload.title}`,
|
||||||
|
description: payload.message,
|
||||||
|
color: payload.level === 'critical' ? 15158332 : payload.level === 'warning' ? 16776960 : 3447003,
|
||||||
|
fields: payload.data ? [
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
value: `
|
||||||
|
${JSON.stringify(payload.data, null, 2).substring(0, 1000)}
|
||||||
|
`,
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
] : [],
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
embeds,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Discord error: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'discord',
|
||||||
|
message: 'Sent to Discord',
|
||||||
|
response: res.statusText,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
export async function sendGotify(
|
||||||
|
config: { gotifyServerUrl: string; gotifyAppToken: string },
|
||||||
|
payload: EventPayload
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
const {gotifyServerUrl, gotifyAppToken} = config;
|
||||||
|
|
||||||
|
const baseUrl = gotifyServerUrl.replace(/\/$/, "");
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
title: `[${payload.level.toUpperCase()}] ${payload.title}`,
|
||||||
|
message: `${payload.message}\n\n${payload.data ? `Data:\n${JSON.stringify(payload.data, null, 2)}` : ''}`,
|
||||||
|
priority: payload.level === 'critical' ? 8 : payload.level === 'warning' ? 5 : 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/message?token=${gotifyAppToken}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Gotify error: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'gotify',
|
||||||
|
message: 'Sent to Gotify',
|
||||||
|
response: await res.json(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
import type {ProviderKind, EventPayload, DispatchResult} from '../types';
|
import type {ProviderKind, EventPayload, DispatchResult} from '../types';
|
||||||
import {sendSlack} from './slack';
|
import {sendSlack} from './slack';
|
||||||
import {sendSmtp} from './smtp';
|
import {sendSmtp} from './smtp';
|
||||||
|
import {sendDiscord} from "@/features/notifications/providers/discord";
|
||||||
|
import {sendTelegram} from "@/features/notifications/providers/telegram";
|
||||||
|
import {sendGotify} from "@/features/notifications/providers/gotify";
|
||||||
|
import {sendNtfy} from "@/features/notifications/providers/ntfy";
|
||||||
|
import {sendWebhook} from "@/features/notifications/providers/webhook";
|
||||||
|
|
||||||
const handlers: Record<
|
const handlers: Record<
|
||||||
ProviderKind,
|
ProviderKind,
|
||||||
@@ -9,6 +14,11 @@ const handlers: Record<
|
|||||||
> = {
|
> = {
|
||||||
slack: sendSlack,
|
slack: sendSlack,
|
||||||
smtp: sendSmtp,
|
smtp: sendSmtp,
|
||||||
|
discord: sendDiscord,
|
||||||
|
telegram: sendTelegram,
|
||||||
|
gotify: sendGotify,
|
||||||
|
ntfy: sendNtfy,
|
||||||
|
webhook: sendWebhook
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function dispatchViaProvider(
|
export async function dispatchViaProvider(
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
export async function sendNtfy(
|
||||||
|
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string },
|
||||||
|
payload: EventPayload
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
const {ntfyServerUrl, ntfyTopic, ntfyToken} = config;
|
||||||
|
|
||||||
|
const baseUrl = (ntfyServerUrl || "https://ntfy.sh").replace(/\/$/, "");
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
topic: ntfyTopic,
|
||||||
|
title: payload.title,
|
||||||
|
message: payload.message + (payload.data ? `\n\nData:\n${JSON.stringify(payload.data, null, 2)}` : ''),
|
||||||
|
priority: 1,
|
||||||
|
tags: [payload.level === 'critical' ? 'rotating_light' : payload.level === 'warning' ? 'warning' : 'information_source'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ntfyToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`ntfy error: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'ntfy',
|
||||||
|
message: 'Sent to ntfy',
|
||||||
|
response: await res.json(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
export async function sendTelegram(
|
||||||
|
config: { telegramBotToken: string; telegramChatId: string },
|
||||||
|
payload: EventPayload
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
const {telegramBotToken, telegramChatId} = config;
|
||||||
|
|
||||||
|
// Helper to escape HTML characters
|
||||||
|
const escapeHtml = (unsafe: string) => {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = escapeHtml(payload.title);
|
||||||
|
const message = escapeHtml(payload.message);
|
||||||
|
const level = escapeHtml(payload.level.toUpperCase());
|
||||||
|
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 body = {
|
||||||
|
chat_id: telegramChatId,
|
||||||
|
text,
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Telegram error: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'telegram',
|
||||||
|
message: 'Sent to Telegram',
|
||||||
|
response: await res.text(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type {EventPayload, DispatchResult} from '../types';
|
||||||
|
|
||||||
|
export async function sendWebhook(
|
||||||
|
config: { webhookUrl: string; webhookSecret?: string; webhookSecretHeader?: string },
|
||||||
|
payload: EventPayload
|
||||||
|
): Promise<DispatchResult> {
|
||||||
|
const {webhookUrl, webhookSecret, webhookSecretHeader} = config;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Portabase-Notifier/1.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (webhookSecret) {
|
||||||
|
headers[webhookSecretHeader || 'X-Webhook-Secret'] = webhookSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Webhook error: ${res.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'webhook',
|
||||||
|
message: 'Sent to Webhook',
|
||||||
|
response: await res.text(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type ProviderKind = 'slack' | 'smtp';
|
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook';
|
||||||
|
|
||||||
export interface DispatchResult {
|
export interface DispatchResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ export async function deleteFileS3Private(fileName: string, bucketName: string)
|
|||||||
*/
|
*/
|
||||||
export async function deleteLocalPrivate(fileName: string) {
|
export async function deleteLocalPrivate(fileName: string) {
|
||||||
try {
|
try {
|
||||||
|
console.log(fileName);
|
||||||
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
|
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
|
||||||
|
console.log(`Deleted ${filePath}`);
|
||||||
// Delete locally
|
// Delete locally
|
||||||
await unlink(filePath);
|
await unlink(filePath);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {Setting} from "@/db/schema/01_setting";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
|
||||||
const privateS3ImageDir = "images/";
|
const imageDir = "images/";
|
||||||
|
|
||||||
|
|
||||||
export const uploadImageAction = userAction
|
export const uploadImageAction = userAction
|
||||||
@@ -39,10 +39,10 @@ export const uploadImageAction = userAction
|
|||||||
let result: void | UploadedObjectInfo;
|
let result: void | UploadedObjectInfo;
|
||||||
|
|
||||||
if (settings.storage === "local") {
|
if (settings.storage === "local") {
|
||||||
fileName = `${uuid}.${fileFormat}`;
|
fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||||
result = await uploadLocal(fileName, buffer);
|
result = await uploadLocal(fileName, buffer);
|
||||||
} else if (settings.storage === "s3") {
|
} else if (settings.storage === "s3") {
|
||||||
fileName = `${privateS3ImageDir}${uuid}.${fileFormat}`;
|
fileName = `${imageDir}${uuid}.${fileFormat}`;
|
||||||
result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
|
result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported storage type: ${settings.storage}`);
|
throw new Error(`Unsupported storage type: ${settings.storage}`);
|
||||||
@@ -54,12 +54,11 @@ export const uploadImageAction = userAction
|
|||||||
|
|
||||||
|
|
||||||
async function uploadLocal(fileName: string, buffer: any) {
|
async function uploadLocal(fileName: string, buffer: any) {
|
||||||
const localDir = "private/uploads/images/";
|
const localDir = "private/uploads/";
|
||||||
try {
|
try {
|
||||||
await mkdir(path.join(process.cwd(), localDir), {recursive: true});
|
await mkdir(path.join(process.cwd(), localDir), {recursive: true});
|
||||||
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
|
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error occured ", error);
|
|
||||||
throw new Error("An error occured while importing image");
|
throw new Error("An error occured while importing image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,15 +45,12 @@ export function useAutoRefresh(options: UseAutoRefreshOptions) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(event.data);
|
payload = JSON.parse(event.data);
|
||||||
} catch {
|
} catch {}
|
||||||
// keep raw payload
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldRefresh =
|
const shouldRefresh =
|
||||||
options.sse?.shouldRefresh?.(payload) ?? true;
|
options.sse?.shouldRefresh?.(payload) ?? true;
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
console.log("Received event source", eventSource);
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-15
@@ -46,11 +46,7 @@ export const auth = betterAuth({
|
|||||||
{}
|
{}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
|
||||||
onPasswordReset: async ({user}, request) => {
|
|
||||||
console.log(`Password for user ${user.email} has been reset.`);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
async sendVerificationEmail({user, token, url}) {
|
async sendVerificationEmail({user, token, url}) {
|
||||||
@@ -176,13 +172,12 @@ export const auth = betterAuth({
|
|||||||
const role = userCount === 0 ? "owner" : "admin";
|
const role = userCount === 0 ? "owner" : "admin";
|
||||||
|
|
||||||
|
|
||||||
const defaultOrgSlug = "default"; // change this if your default org has a different slug
|
const defaultOrgSlug = "default";
|
||||||
const defaultOrg = await db.query.organization.findFirst({
|
const defaultOrg = await db.query.organization.findFirst({
|
||||||
where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug),
|
where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (defaultOrg) {
|
if (defaultOrg) {
|
||||||
console.log(user)
|
|
||||||
await db.insert(drizzleDb.schemas.member).values({
|
await db.insert(drizzleDb.schemas.member).values({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
organizationId: defaultOrg.id,
|
organizationId: defaultOrg.id,
|
||||||
@@ -295,7 +290,6 @@ export const auth = betterAuth({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("sessionId", session.id);
|
|
||||||
|
|
||||||
const [aa] = await db
|
const [aa] = await db
|
||||||
.update(drizzleUser.session)
|
.update(drizzleUser.session)
|
||||||
@@ -303,7 +297,6 @@ export const auth = betterAuth({
|
|||||||
.where(eq(drizzleUser.session.id, session.id))
|
.where(eq(drizzleUser.session.id, session.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
console.log("aaaaa", aa);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
@@ -516,9 +509,7 @@ export const checkSlugOrganization = async (slug: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
} catch (e) {
|
} catch {}
|
||||||
console.log("err", e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getActiveMember = async () => {
|
export const getActiveMember = async () => {
|
||||||
@@ -542,7 +533,5 @@ export const setActiveOrganization = async (slug: string) => {
|
|||||||
organizationSlug: slug,
|
organizationSlug: slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {}
|
||||||
console.log("error", e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const retentionCleanTask = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(databases);
|
|
||||||
|
|
||||||
for (const db of databases) {
|
for (const db of databases) {
|
||||||
if (!db.retentionPolicy) continue; // no policy = skip
|
if (!db.retentionPolicy) continue; // no policy = skip
|
||||||
@@ -34,7 +33,6 @@ export async function enforceRetention(
|
|||||||
databaseId: string,
|
databaseId: string,
|
||||||
policy: typeof retentionPolicy.$inferSelect
|
policy: typeof retentionPolicy.$inferSelect
|
||||||
) {
|
) {
|
||||||
console.log(`EnforceRetention: ${policy}`);
|
|
||||||
|
|
||||||
switch (policy.type) {
|
switch (policy.type) {
|
||||||
case "count":
|
case "count":
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user