Compare commits
190 Commits
add-some-t
...
main
Author | SHA1 | Date | |
---|---|---|---|
32e2abdd6e | |||
fb35acdb20 | |||
![]() |
8afaae2dca | ||
![]() |
2569a346ca | ||
![]() |
cb6b73079e | ||
![]() |
fc85f447b6 | ||
![]() |
309df5a83a | ||
![]() |
76d2b8bf44 | ||
![]() |
c5b2a17a3f | ||
![]() |
388eaf4516 | ||
![]() |
ad69afdef3 | ||
![]() |
f077bef812 | ||
![]() |
3c82873568 | ||
![]() |
fcc6384500 | ||
![]() |
93b964d911 | ||
![]() |
07e6d9def8 | ||
![]() |
b45dc08482 | ||
![]() |
7626c150ed | ||
![]() |
5b4a638f12 | ||
![]() |
7cafde91aa | ||
![]() |
2811e8dacb | ||
![]() |
354a37aaf7 | ||
![]() |
bb001add3b | ||
![]() |
7a63e0a123 | ||
![]() |
6b5ef04ade | ||
![]() |
443dae6e15 | ||
![]() |
dd2389f877 | ||
![]() |
dd62bbbbb2 | ||
![]() |
ae521fa55d | ||
![]() |
d62530b2ab | ||
![]() |
09abb12d39 | ||
![]() |
4ae0d3f86e | ||
![]() |
5b4ee7e23b | ||
![]() |
0f6958dcfe | ||
![]() |
2b0fad626a | ||
![]() |
2050d59fd7 | ||
![]() |
431f2e2600 | ||
![]() |
b8599f046b | ||
![]() |
cb33f6df96 | ||
![]() |
82673a5d32 | ||
![]() |
2ff2813f81 | ||
![]() |
c0718d2eb8 | ||
![]() |
d2d8102898 | ||
![]() |
91dd7db948 | ||
![]() |
3ba6057297 | ||
![]() |
0b4ec2627f | ||
![]() |
51a8eb2250 | ||
![]() |
3611ee5973 | ||
![]() |
49d10d8d5f | ||
![]() |
bf742aaf70 | ||
![]() |
a4e7f03b84 | ||
![]() |
ea9579db68 | ||
![]() |
10468ea58b | ||
![]() |
cd5251232c | ||
![]() |
e0c880fb43 | ||
![]() |
8c427e2988 | ||
![]() |
b7f6da5aa0 | ||
![]() |
7d3e0cd9cd | ||
![]() |
c0ae4b60aa | ||
![]() |
4aad198612 | ||
![]() |
038d9614ee | ||
![]() |
5ab65f1f3a | ||
![]() |
903f54d2bb | ||
![]() |
451c2d8feb | ||
![]() |
68696c7d20 | ||
![]() |
3cfefebb44 | ||
![]() |
7e695a3b2c | ||
![]() |
3fb50189bc | ||
![]() |
4691c5d48c | ||
![]() |
cbef6e70b8 | ||
![]() |
e0fd78eb8c | ||
![]() |
c092e5b150 | ||
![]() |
a8f39c2cc1 | ||
![]() |
32c912d982 | ||
![]() |
22118c5808 | ||
![]() |
09178ca15c | ||
![]() |
5a6513c218 | ||
![]() |
3387703482 | ||
![]() |
2ec7860ce1 | ||
![]() |
60b9f52f01 | ||
![]() |
aa0cad50a2 | ||
![]() |
d5ec883f23 | ||
![]() |
6b99f9854f | ||
![]() |
c4369c3a2e | ||
![]() |
444e56bb5a | ||
![]() |
2dc2583146 | ||
![]() |
ffa966c434 | ||
![]() |
7c0c9e8d0c | ||
![]() |
30e522da13 | ||
![]() |
685eb338bb | ||
![]() |
f3f889d46a | ||
![]() |
d791fce509 | ||
![]() |
72d2205d79 | ||
![]() |
db2814ec96 | ||
![]() |
159303b6a3 | ||
![]() |
5ad2820e8c | ||
![]() |
234e7c19f8 | ||
![]() |
68abbc368c | ||
![]() |
39d8f481e0 | ||
![]() |
7edfcfa440 | ||
![]() |
bad79df298 | ||
![]() |
ef41275cf0 | ||
![]() |
26519b9482 | ||
![]() |
ddb84fc9cc | ||
![]() |
752dc7a4cf | ||
![]() |
daa22f7e54 | ||
![]() |
62791a76f3 | ||
![]() |
82ea3a553b | ||
![]() |
0850ef5dd2 | ||
![]() |
e1721df11c | ||
![]() |
79883f1f09 | ||
![]() |
f7187eb4cf | ||
![]() |
96fd25d1cc | ||
![]() |
d89af10b49 | ||
![]() |
3062833b77 | ||
![]() |
b3a97fcccb | ||
![]() |
52ffb80f35 | ||
![]() |
03bc1e3323 | ||
![]() |
2d9ff83bbd | ||
![]() |
680fa3c0ee | ||
![]() |
ddf835c4d1 | ||
![]() |
ae50f2ee5d | ||
![]() |
d95c10dfba | ||
![]() |
590ee8c0fe | ||
![]() |
ca10f04f24 | ||
![]() |
d2f04fc031 | ||
![]() |
cb96e4f736 | ||
![]() |
14bc205ef2 | ||
![]() |
31356c0bdc | ||
![]() |
ab247891dc | ||
![]() |
5165625cd0 | ||
![]() |
cc74071933 | ||
![]() |
273e7b11ad | ||
![]() |
55e7d53769 | ||
![]() |
9c1838031c | ||
![]() |
3b61e6e02c | ||
![]() |
a4e4d37413 | ||
![]() |
4127954c1d | ||
![]() |
554d2cd132 | ||
![]() |
d2326a4cf3 | ||
![]() |
7c0c9f922d | ||
![]() |
4dd3aa2375 | ||
![]() |
05d3740b80 | ||
![]() |
e425efa26a | ||
![]() |
cd8e35ba27 | ||
![]() |
710cc57c4f | ||
![]() |
249bc4d0c2 | ||
![]() |
aa6567a490 | ||
![]() |
f40cb4839e | ||
![]() |
d5df57b7ec | ||
![]() |
5f89026db3 | ||
![]() |
10aaef98b0 | ||
![]() |
c323e170ac | ||
![]() |
797b924a62 | ||
![]() |
f1023ebc64 | ||
![]() |
e15411a04c | ||
![]() |
3902dcd3d1 | ||
![]() |
9c911abfb7 | ||
![]() |
23c39cede8 | ||
![]() |
50e62f0b6c | ||
![]() |
d7d1d3c5b5 | ||
![]() |
16d339c6f3 | ||
![]() |
9b0786f699 | ||
![]() |
fc1b20e95b | ||
![]() |
ef8ae9b38f | ||
![]() |
7a286ad506 | ||
![]() |
f7d46a4cec | ||
![]() |
4ac465f16c | ||
![]() |
c82d8653fd | ||
![]() |
949c2e8809 | ||
![]() |
20a1cec358 | ||
![]() |
21240b8ada | ||
![]() |
f218363e97 | ||
![]() |
1a5fd713da | ||
![]() |
3d6202f335 | ||
![]() |
9161e26d3a | ||
![]() |
b00d810fae | ||
![]() |
9a4b995d09 | ||
![]() |
c97405b622 | ||
![]() |
b5ebd9173e | ||
![]() |
a1ff4dc9ef | ||
![]() |
2298fee054 | ||
![]() |
e236126a30 | ||
![]() |
7da5265575 | ||
![]() |
62fc97a417 | ||
![]() |
fa3f2437a3 | ||
![]() |
8dc5238fcb | ||
![]() |
238350b940 | ||
![]() |
99bf7b1889 | ||
![]() |
d72c91644d |
33
.gitea/workflows/build.yml
Normal file
33
.gitea/workflows/build.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Build Application
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: large
|
||||||
|
steps:
|
||||||
|
|
||||||
|
# Шаг 1: Checkout кода
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Шаг 2: Логин в Docker Registry (Gitea)
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.GIT_INSTANCE }} # Замените на адрес вашего Gitea
|
||||||
|
username: ${{ secrets.GIT_USERNAME }}
|
||||||
|
password: ${{ secrets.GIT_TOKEN }}
|
||||||
|
|
||||||
|
# Шаг 3: Сборка и пуш Docker образа
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ vars.GIT_INSTANCE }}/${{ secrets.GIT_USERNAME }}/${{ vars.REPOSITORY }}:latest
|
||||||
|
${{ vars.GIT_INSTANCE }}/${{ secrets.GIT_USERNAME }}/${{ vars.REPOSITORY }}:${{ github.sha }}
|
63
.github/CONTRIBUTING.md
vendored
63
.github/CONTRIBUTING.md
vendored
@ -1,63 +0,0 @@
|
|||||||
# Contribution Guidelines
|
|
||||||
|
|
||||||
Table of Contents:
|
|
||||||
|
|
||||||
<!-- vim-markdown-toc GFM -->
|
|
||||||
|
|
||||||
* [Did you find a bug?](#did-you-find-a-bug)
|
|
||||||
* [Is it a Security Vulnerability?](#is-it-a-security-vulnerability)
|
|
||||||
* [Is it already a known issue?](#is-it-already-a-known-issue)
|
|
||||||
* [Reporting a Bug](#reporting-a-bug)
|
|
||||||
* [Is there a patch for the bug?](#is-there-a-patch-for-the-bug)
|
|
||||||
* [Do you want to add a new feature?](#do-you-want-to-add-a-new-feature)
|
|
||||||
* [Is it just an idea?](#is-it-just-an-idea)
|
|
||||||
* [Is there a patch for the feature?](#is-there-a-patch-for-the-feature)
|
|
||||||
* [Do you have questions about the Synapse Admin project or need guidance?](#do-you-have-questions-about-the-synapse-admin-project-or-need-guidance)
|
|
||||||
|
|
||||||
<!-- vim-markdown-toc -->
|
|
||||||
|
|
||||||
## Did you find a bug?
|
|
||||||
|
|
||||||
### Is it a Security Vulnerability?
|
|
||||||
|
|
||||||
Please follow the [Security Policy](https://github.com/etkecc/synapse-admin/blob/main/.github/SECURITY.md) for reporting
|
|
||||||
security vulnerabilities.
|
|
||||||
|
|
||||||
### Is it already a known issue?
|
|
||||||
|
|
||||||
Please ensure the bug was not already reported by searching [the Issues section](https://github.com/etkecc/synapse-admin/issues).
|
|
||||||
|
|
||||||
### Reporting a Bug
|
|
||||||
|
|
||||||
If you think you have found a bug in Synapse Admin, it is not a security vulnerability, and it is not already reported,
|
|
||||||
please open [a new issue](https://github.com/etkecc/synapse-admin/issues/new) with:
|
|
||||||
* A proper title and clear description of the problem.
|
|
||||||
* As much relevant information as possible:
|
|
||||||
* The version of Synapse Admin you are using.
|
|
||||||
* The version of Synapse you are using.
|
|
||||||
* Any relevant browser console logs, failed requests details, and error messages.
|
|
||||||
|
|
||||||
### Is there a patch for the bug?
|
|
||||||
|
|
||||||
If you already have a patch for the bug, please open a pull request with the patch,
|
|
||||||
and mention the issue number in the pull request description.
|
|
||||||
|
|
||||||
## Do you want to add a new feature?
|
|
||||||
|
|
||||||
### Is it just an idea?
|
|
||||||
|
|
||||||
Please open [a new issue](https://github.com/etkecc/synapse-admin/issues/new) with:
|
|
||||||
* A proper title and clear description of the requested feature.
|
|
||||||
* Any relevant information about the feature:
|
|
||||||
* Why do you think this feature is needed?
|
|
||||||
* How do you think it should work? (provide Synapse Admin API endpoint)
|
|
||||||
* Any relevant screenshots or mockups.
|
|
||||||
|
|
||||||
### Is there a patch for the feature?
|
|
||||||
|
|
||||||
If you already have a patch for the feature, please open a pull request with the patch,
|
|
||||||
and mention the issue number in the pull request description.
|
|
||||||
|
|
||||||
## Do you have questions about the Synapse Admin project or need guidance?
|
|
||||||
|
|
||||||
Please use the official community Matrix room: [#synapse-admin:etke.cc](https://matrix.to/#/#synapse-admin:etke.cc)
|
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
liberapay: etkecc
|
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Report a Synapse Admin bug
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Browser console logs**
|
|
||||||
If applicable, add the browser console's log
|
|
||||||
|
|
||||||
**Instance configuration:**
|
|
||||||
- Synapse Admin version: [e.g. v0.10.3-etke39]
|
|
||||||
- Synapse version [v1.127.1]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for Synapse Admin
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Provide related Synapse Admin API endpoints**
|
|
||||||
If applicable, provide links to the Synapse Admin API's endpoints that could be used to implement that feature
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
14
.github/SECURITY.md
vendored
14
.github/SECURITY.md
vendored
@ -1,14 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Only [the last published version](https://github.com/etkecc/synapse-admin/releases/latest) of the project is supported.
|
|
||||||
This means that only the latest version will receive security updates.
|
|
||||||
If you are using an older version, you are strongly encouraged to upgrade to the latest version.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please contact us using the [#synapse-admin:etke.cc](https://matrix.to/#/#synapse-admin:etke.cc) Matrix room.
|
|
||||||
The Synapse Admin project is a static JS UI for the Synapse server,
|
|
||||||
so it is unlikely that there are (or will be) any impactful security vulnerabilities in the project itself.
|
|
||||||
However, we do not rule out the possibility of such cases, so we will be happy to receive any reports!
|
|
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@ -1,19 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 30
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 30
|
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 30
|
|
121
.github/workflows/workflow.yml
vendored
121
.github/workflows/workflow.yml
vendored
@ -1,121 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
tags: [ "v*" ]
|
|
||||||
env:
|
|
||||||
bunny_version: v0.1.0
|
|
||||||
base_path: ./
|
|
||||||
permissions:
|
|
||||||
checks: write
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
pull-requests: read
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
cache: yarn
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable --network-timeout=300000
|
|
||||||
- name: Build
|
|
||||||
run: yarn build --base=${{ env.base_path }}
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
path: dist/
|
|
||||||
name: dist
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
compression-level: 0
|
|
||||||
overwrite: true
|
|
||||||
include-hidden-files: true
|
|
||||||
|
|
||||||
docker:
|
|
||||||
name: Docker
|
|
||||||
needs: build
|
|
||||||
runs-on: self-hosted
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist
|
|
||||||
path: dist/
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
|
||||||
- name: Login to ghcr.io
|
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Login to hub.docker.com
|
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
|
||||||
with:
|
|
||||||
username: etkecc
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ github.repository }}
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
registry.etke.cc/${{ github.repository }}
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
|
|
||||||
type=semver,pattern={{raw}}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
cdn:
|
|
||||||
name: CDN
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist
|
|
||||||
path: dist/
|
|
||||||
- name: Upload
|
|
||||||
run: |
|
|
||||||
wget -O bunny-upload.tar.gz https://github.com/etkecc/bunny-upload/releases/download/${{ env.bunny_version }}/bunny-upload_Linux_x86_64.tar.gz
|
|
||||||
tar -xzf bunny-upload.tar.gz
|
|
||||||
echo "${{ secrets.BUNNY_CONFIG }}" > bunny-config.yaml
|
|
||||||
./bunny-upload -c bunny-config.yaml
|
|
||||||
|
|
||||||
github-release:
|
|
||||||
name: Github Release
|
|
||||||
needs: build
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dist
|
|
||||||
path: dist/
|
|
||||||
- name: Prepare release
|
|
||||||
run: |
|
|
||||||
mv dist synapse-admin
|
|
||||||
tar chvzf synapse-admin.tar.gz synapse-admin
|
|
||||||
- uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
|
||||||
with:
|
|
||||||
files: synapse-admin.tar.gz
|
|
||||||
generate_release_notes: true
|
|
||||||
make_latest: "true"
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
12
Dockerfile
12
Dockerfile
@ -1,5 +1,11 @@
|
|||||||
|
FROM node:lts AS builder
|
||||||
|
ARG BASE_PATH=./
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
RUN yarn config set enableTelemetry 0 && \
|
||||||
|
yarn install --immutable --network-timeout=300000 && \
|
||||||
|
yarn build --base=$BASE_PATH
|
||||||
|
|
||||||
FROM ghcr.io/static-web-server/static-web-server:2
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
|
|
||||||
ENV SERVER_ROOT=/app
|
ENV SERVER_ROOT=/app
|
||||||
|
COPY --from=builder /src/dist /app
|
||||||
COPY ./dist /app
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
FROM node:lts AS builder
|
|
||||||
ARG BASE_PATH=./
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . /src
|
|
||||||
RUN yarn config set enableTelemetry 0 && \
|
|
||||||
yarn install --immutable --network-timeout=300000 && \
|
|
||||||
yarn build --base=$BASE_PATH
|
|
||||||
|
|
||||||
FROM ghcr.io/static-web-server/static-web-server:2
|
|
||||||
ENV SERVER_ROOT=/app
|
|
||||||
COPY --from=builder /src/dist /app
|
|
5
Dockerfile.old
Normal file
5
Dockerfile.old
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
|
|
||||||
|
ENV SERVER_ROOT=/app
|
||||||
|
|
||||||
|
COPY ./dist /app
|
@ -113,6 +113,11 @@ The following changes are already implemented:
|
|||||||
* 🗂️ [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276)
|
* 🗂️ [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276)
|
||||||
* 🧾 [Make bulk registration CSV import more user-friendly](https://github.com/etkecc/synapse-admin/pull/411)
|
* 🧾 [Make bulk registration CSV import more user-friendly](https://github.com/etkecc/synapse-admin/pull/411)
|
||||||
* 🌐 [Configurable CORS Credentials](https://github.com/etkecc/synapse-admin/pull/456)
|
* 🌐 [Configurable CORS Credentials](https://github.com/etkecc/synapse-admin/pull/456)
|
||||||
|
* 🧪 [Do not check homeserver URL during typing in the login form](https://github.com/etkecc/synapse-admin/pull/585)
|
||||||
|
* 🔧 [Improve user account status toggles](https://github.com/etkecc/synapse-admin/pull/608)
|
||||||
|
* 🛡️ [Validate that password is entered upon reactivation of account](https://github.com/etkecc/synapse-admin/pull/609)
|
||||||
|
* 🌏 [Add Japanese localization](https://github.com/etkecc/synapse-admin/pull/631)
|
||||||
|
* 🗣️ [Correctly set document language based on the selected locale](https://github.com/etkecc/synapse-admin/issues/723)
|
||||||
|
|
||||||
#### exclusive for [etke.cc](https://etke.cc) customers
|
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||||
|
|
||||||
@ -123,6 +128,7 @@ The following list contains such features - they are only available for [etke.cc
|
|||||||
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
||||||
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
* 🛠️ [Server Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
||||||
* 🚀 [Server Actions page](https://github.com/etkecc/synapse-admin/pull/457)
|
* 🚀 [Server Actions page](https://github.com/etkecc/synapse-admin/pull/457)
|
||||||
|
* 💳 [Billing page](https://github.com/etkecc/synapse-admin/pull/691)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ You can do that for a single homeserver or multiple homeservers at once, as `res
|
|||||||
an array of strings.
|
an array of strings.
|
||||||
|
|
||||||
The examples below contain the configuration settings to restrict the Synapse Admin instance to work only with
|
The examples below contain the configuration settings to restrict the Synapse Admin instance to work only with
|
||||||
`example.com` (with Synapse runing at `matrix.example.com`) and
|
`example.com` (with Synapse running at `matrix.example.com`) and
|
||||||
`example.net` (with Synapse running at `synapse.example.net`) homeservers.
|
`example.net` (with Synapse running at `synapse.example.net`) homeservers.
|
||||||
Note that the homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
|
Note that the homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ const config: JestConfigWithTsJest = {
|
|||||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.tsx?$": [
|
"^.+\\.(js|jsx|ts|tsx)$": [
|
||||||
"ts-jest",
|
"ts-jest",
|
||||||
{
|
{
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
@ -27,5 +27,6 @@ const config: JestConfigWithTsJest = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
transformIgnorePatterns: [],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
3
justfile
3
justfile
@ -6,6 +6,9 @@ default:
|
|||||||
build: __install
|
build: __install
|
||||||
@yarn run build --base=./
|
@yarn run build --base=./
|
||||||
|
|
||||||
|
update:
|
||||||
|
yarn upgrade-interactive --latest
|
||||||
|
|
||||||
# run the app in a development mode
|
# run the app in a development mode
|
||||||
run:
|
run:
|
||||||
@yarn start --host 0.0.0.0
|
@yarn start --host 0.0.0.0
|
||||||
|
108
package.json
108
package.json
@ -1,75 +1,95 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.10.3",
|
"version": "0.11.1",
|
||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Feature-packed and visually customizable admin GUI for Matrix Synapse servers.",
|
||||||
|
"keywords": [
|
||||||
|
"matrix",
|
||||||
|
"synapse",
|
||||||
|
"admin",
|
||||||
|
"homeserver",
|
||||||
|
"management",
|
||||||
|
"react",
|
||||||
|
"nodejs",
|
||||||
|
"dashboard",
|
||||||
|
"etkecc",
|
||||||
|
"docker"
|
||||||
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
|
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"homepage": ".",
|
"homepage": "https://github.com/etkecc/synapse-admin#readme",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/etkecc/synapse-admin"
|
"url": "git+https://github.com/etkecc/synapse-admin.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/etkecc/synapse-admin/issues"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://liberapay.com/etkecc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.32.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.4",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.15.19",
|
"@types/node": "^24.2.1",
|
||||||
"@types/papaparse": "^5.3.16",
|
"@types/papaparse": "^5.3.16",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||||
"@typescript-eslint/parser": "^8.32.0",
|
"@typescript-eslint/parser": "^8.37.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-prettier": "^5.4.0",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.0.5",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^30.0.5",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"react-test-renderer": "^19.1.0",
|
"react-test-renderer": "^19.1.1",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.4.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.39.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.1.2",
|
||||||
"vite-plugin-version-mark": "^0.1.4"
|
"vite-plugin-version-mark": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bicstone/ra-language-japanese": "^5.10.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@haleos/ra-language-german": "^1.0.0",
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^6.4.8",
|
"@mui/icons-material": "^7.3.1",
|
||||||
"@mui/material": "^6.4.8",
|
"@mui/material": "^7.3.1",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.0",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.84.2",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"jest-fixed-jsdom": "^0.0.9",
|
"jest-fixed-jsdom": "^0.0.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.5.1",
|
"papaparse": "^5.5.3",
|
||||||
"ra-core": "^5.4.4",
|
"ra-core": "^5.4.4",
|
||||||
"ra-i18n-polyglot": "^5.4.4",
|
"ra-i18n-polyglot": "^5.4.4",
|
||||||
"ra-language-english": "^5.4.4",
|
"ra-language-english": "^5.4.4",
|
||||||
"ra-language-farsi": "^5.1.0",
|
"ra-language-farsi": "^5.1.0",
|
||||||
"ra-language-french": "^5.8.2",
|
"ra-language-french": "^5.10.1",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^5.4.3",
|
"ra-language-russian": "^5.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.1",
|
||||||
"react-admin": "^5.8.2",
|
"react-admin": "^5.10.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-is": "^19.1.0",
|
"react-is": "^19.1.1",
|
||||||
"ts-jest-mock-import-meta": "^1.3.0",
|
|
||||||
"react-router": "^7.6.0",
|
"react-router": "^7.6.0",
|
||||||
"react-router-dom": "^7.6.0"
|
"react-router-dom": "^7.8.0",
|
||||||
|
"ts-jest-mock-import-meta": "^1.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
@ -102,6 +122,18 @@
|
|||||||
"root": true,
|
"root": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"args": "all",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"caughtErrors": "all",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"import/no-extraneous-dependencies": [
|
"import/no-extraneous-dependencies": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
BIN
screenshots/etke.cc/billing/page.webp
Normal file
BIN
screenshots/etke.cc/billing/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
12
src/App.tsx
12
src/App.tsx
@ -5,6 +5,7 @@ import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin
|
|||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
import AdminLayout from "./components/AdminLayout";
|
import AdminLayout from "./components/AdminLayout";
|
||||||
|
import BillingPage from "./components/etke.cc/BillingPage";
|
||||||
import ServerActionsPage from "./components/etke.cc/ServerActionsPage";
|
import ServerActionsPage from "./components/etke.cc/ServerActionsPage";
|
||||||
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
@ -16,6 +17,7 @@ import germanMessages from "./i18n/de";
|
|||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
import frenchMessages from "./i18n/fr";
|
import frenchMessages from "./i18n/fr";
|
||||||
import italianMessages from "./i18n/it";
|
import italianMessages from "./i18n/it";
|
||||||
|
import japaneseMessages from "./i18n/ja";
|
||||||
import russianMessages from "./i18n/ru";
|
import russianMessages from "./i18n/ru";
|
||||||
import chineseMessages from "./i18n/zh";
|
import chineseMessages from "./i18n/zh";
|
||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
@ -35,6 +37,7 @@ const messages = {
|
|||||||
en: englishMessages,
|
en: englishMessages,
|
||||||
fr: frenchMessages,
|
fr: frenchMessages,
|
||||||
it: italianMessages,
|
it: italianMessages,
|
||||||
|
ja: japaneseMessages,
|
||||||
ru: russianMessages,
|
ru: russianMessages,
|
||||||
zh: chineseMessages,
|
zh: chineseMessages,
|
||||||
};
|
};
|
||||||
@ -46,9 +49,10 @@ const i18nProvider = polyglotI18nProvider(
|
|||||||
{ locale: "de", name: "Deutsch" },
|
{ locale: "de", name: "Deutsch" },
|
||||||
{ locale: "fr", name: "Français" },
|
{ locale: "fr", name: "Français" },
|
||||||
{ locale: "it", name: "Italiano" },
|
{ locale: "it", name: "Italiano" },
|
||||||
{ locale: "fa", name: "Persian(فارسی)" },
|
{ locale: "ja", name: "Japanese (日本語)" },
|
||||||
{ locale: "ru", name: "Russian(Русский)" },
|
{ locale: "fa", name: "Persian (فارسی)" },
|
||||||
{ locale: "zh", name: "简体中文" },
|
{ locale: "ru", name: "Russian (Русский)" },
|
||||||
|
{ locale: "zh", name: "Chinese (简体中文)" },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -59,6 +63,7 @@ export const App = () => (
|
|||||||
<Admin
|
<Admin
|
||||||
disableTelemetry
|
disableTelemetry
|
||||||
requireAuth
|
requireAuth
|
||||||
|
title="Synapse Admin"
|
||||||
layout={AdminLayout}
|
layout={AdminLayout}
|
||||||
loginPage={LoginPage}
|
loginPage={LoginPage}
|
||||||
authProvider={authProvider}
|
authProvider={authProvider}
|
||||||
@ -75,6 +80,7 @@ export const App = () => (
|
|||||||
<Route path="/server_actions/recurring/:id" element={<RecurringCommandEdit />} />
|
<Route path="/server_actions/recurring/:id" element={<RecurringCommandEdit />} />
|
||||||
<Route path="/server_actions/recurring/create" element={<RecurringCommandEdit />} />
|
<Route path="/server_actions/recurring/create" element={<RecurringCommandEdit />} />
|
||||||
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||||
|
<Route path="/billing" element={<BillingPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import ManageHistoryIcon from "@mui/icons-material/ManageHistory";
|
import ManageHistoryIcon from "@mui/icons-material/ManageHistory";
|
||||||
|
import PaymentIcon from "@mui/icons-material/Payment";
|
||||||
import { useEffect, useState, Suspense } from "react";
|
import { useEffect, useState, Suspense } from "react";
|
||||||
import {
|
import {
|
||||||
CheckForApplicationUpdate,
|
CheckForApplicationUpdate,
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
useLogout,
|
useLogout,
|
||||||
UserMenu,
|
UserMenu,
|
||||||
useStore,
|
useStore,
|
||||||
|
useLocaleState,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
@ -83,11 +85,11 @@ const AdminMenu = props => {
|
|||||||
setEtkeRoutesEnabled(true);
|
setEtkeRoutesEnabled(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
const [serverStatus, _setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||||
success: false,
|
success: false,
|
||||||
ok: false,
|
ok: false,
|
||||||
host: "",
|
host: "",
|
||||||
@ -120,10 +122,12 @@ const AdminMenu = props => {
|
|||||||
primaryText="Server Actions"
|
primaryText="Server Actions"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{etkeRoutesEnabled && <Menu.Item key="billing" to="/billing" leftIcon={<PaymentIcon />} primaryText="Billing" />}
|
||||||
<Menu.ResourceItems />
|
<Menu.ResourceItems />
|
||||||
{menu &&
|
{menu &&
|
||||||
menu.map((item, index) => {
|
menu.map((item, index) => {
|
||||||
const { url, icon, label } = item;
|
const { url, icon, label } = item;
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -143,6 +147,12 @@ const AdminMenu = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AdminLayout = ({ children }) => {
|
export const AdminLayout = ({ children }) => {
|
||||||
|
// Set the document language based on the selected locale
|
||||||
|
const [locale, _setLocale] = useLocaleState();
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = locale;
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout
|
<Layout
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import ActionCheck from "@mui/icons-material/CheckCircle";
|
import ActionCheck from "@mui/icons-material/CheckCircle";
|
||||||
import ActionDelete from "@mui/icons-material/Delete";
|
import ActionDelete from "@mui/icons-material/Delete";
|
||||||
import AlertError from "@mui/icons-material/ErrorOutline";
|
import AlertError from "@mui/icons-material/ErrorOutline";
|
||||||
import { Button, Tooltip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import {
|
import {
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
RaRecord,
|
|
||||||
useNotify,
|
useNotify,
|
||||||
useRedirect,
|
useRedirect,
|
||||||
useDelete,
|
|
||||||
NotificationType,
|
NotificationType,
|
||||||
useDeleteMany,
|
useDeleteMany,
|
||||||
Identifier,
|
Identifier,
|
||||||
@ -51,7 +49,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
|
|||||||
unselectAll();
|
unselectAll();
|
||||||
redirect("/rooms");
|
redirect("/rooms");
|
||||||
},
|
},
|
||||||
onError: error => notify("resources.rooms.action.erase.failure", { type: "error" as NotificationType }),
|
onError: _error => notify("resources.rooms.action.erase.failure", { type: "error" as NotificationType }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -63,24 +61,22 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Tooltip title={translate("ra.action.delete")}>
|
<Button
|
||||||
<Button
|
onClick={handleDialogOpen}
|
||||||
onClick={handleDialogOpen}
|
disabled={isLoading}
|
||||||
disabled={isLoading}
|
className={"ra-delete-button"}
|
||||||
className={"ra-delete-button"}
|
key="button"
|
||||||
key="button"
|
size="small"
|
||||||
size="small"
|
sx={{
|
||||||
sx={{
|
"&.MuiButton-sizeSmall": {
|
||||||
"&.MuiButton-sizeSmall": {
|
lineHeight: 1.5,
|
||||||
lineHeight: 1.5,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
color={"error"}
|
||||||
color={"error"}
|
startIcon={<ActionDelete />}
|
||||||
startIcon={<ActionDelete />}
|
>
|
||||||
>
|
{translate("ra.action.delete")}
|
||||||
{translate("ra.action.delete")}
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog open={open} onClose={handleDialogClose}>
|
<Dialog open={open} onClose={handleDialogClose}>
|
||||||
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import ActionCheck from "@mui/icons-material/CheckCircle";
|
import ActionCheck from "@mui/icons-material/CheckCircle";
|
||||||
import ActionDelete from "@mui/icons-material/Delete";
|
import ActionDelete from "@mui/icons-material/Delete";
|
||||||
import AlertError from "@mui/icons-material/ErrorOutline";
|
import AlertError from "@mui/icons-material/ErrorOutline";
|
||||||
import { Tooltip, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import {
|
import {
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
RaRecord,
|
|
||||||
useNotify,
|
useNotify,
|
||||||
useRedirect,
|
useRedirect,
|
||||||
useDelete,
|
|
||||||
NotificationType,
|
NotificationType,
|
||||||
useDeleteMany,
|
useDeleteMany,
|
||||||
Identifier,
|
Identifier,
|
||||||
@ -57,7 +55,7 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
|
|||||||
unselectAll();
|
unselectAll();
|
||||||
redirect("/users");
|
redirect("/users");
|
||||||
},
|
},
|
||||||
onError: error => notify("ra.notification.data_provider_error", { type: "error" as NotificationType }),
|
onError: _error => notify("ra.notification.data_provider_error", { type: "error" as NotificationType }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -69,24 +67,22 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Tooltip title={translate("ra.action.delete")}>
|
<Button
|
||||||
<Button
|
onClick={handleDialogOpen}
|
||||||
onClick={handleDialogOpen}
|
disabled={isLoading}
|
||||||
disabled={isLoading}
|
className={"ra-delete-button"}
|
||||||
className={"ra-delete-button"}
|
key="button"
|
||||||
key="button"
|
size="small"
|
||||||
size="small"
|
sx={{
|
||||||
sx={{
|
"&.MuiButton-sizeSmall": {
|
||||||
"&.MuiButton-sizeSmall": {
|
lineHeight: 1.5,
|
||||||
lineHeight: 1.5,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
color={"error"}
|
||||||
color={"error"}
|
startIcon={<ActionDelete />}
|
||||||
startIcon={<ActionDelete />}
|
>
|
||||||
>
|
{translate("ra.action.delete")}
|
||||||
{translate("ra.action.delete")}
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog open={open} onClose={handleDialogClose}>
|
<Dialog open={open} onClose={handleDialogClose}>
|
||||||
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { Tooltip } from "@mui/material";
|
|
||||||
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
||||||
import { useTranslate } from "react-admin";
|
|
||||||
|
|
||||||
import { isASManaged } from "../utils/mxid";
|
import { isASManaged } from "../utils/mxid";
|
||||||
|
|
||||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||||
const translate = useTranslate();
|
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
@ -15,23 +12,19 @@ export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<DeleteWithConfirmButton
|
||||||
title={isASManagedUser ? translate("resources.devices.action.erase.disabled") : translate("ra.action.delete")}
|
{...props}
|
||||||
>
|
label="ra.action.remove"
|
||||||
<DeleteWithConfirmButton
|
confirmTitle="resources.devices.action.erase.title"
|
||||||
{...props}
|
confirmContent="resources.devices.action.erase.content"
|
||||||
label="ra.action.remove"
|
mutationMode="pessimistic"
|
||||||
confirmTitle="resources.devices.action.erase.title"
|
redirect={false}
|
||||||
confirmContent="resources.devices.action.erase.content"
|
disabled={isASManagedUser}
|
||||||
mutationMode="pessimistic"
|
translateOptions={{
|
||||||
redirect={false}
|
id: record.id,
|
||||||
disabled={isASManagedUser}
|
name: record.display_name ? record.display_name : record.id,
|
||||||
translateOptions={{
|
}}
|
||||||
id: record.id,
|
/>
|
||||||
name: record.display_name ? record.display_name : record.id,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Tooltip, Stack, Switch, Typography } from "@mui/material";
|
import { Stack, Switch, Typography } from "@mui/material";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRecordContext, useTranslate, useNotify, useDataProvider } from "react-admin";
|
import { useRecordContext } from "react-admin";
|
||||||
|
import { useNotify } from "react-admin";
|
||||||
|
import { useDataProvider } from "react-admin";
|
||||||
|
|
||||||
import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider";
|
import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider";
|
||||||
|
|
||||||
@ -13,7 +15,6 @@ const ExperimentalFeatureRow = (props: {
|
|||||||
featureValue: boolean;
|
featureValue: boolean;
|
||||||
updateFeature: (feature_name: string, feature_value: boolean) => void;
|
updateFeature: (feature_name: string, feature_value: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const translate = useTranslate();
|
|
||||||
const featureKey = props.featureKey;
|
const featureKey = props.featureKey;
|
||||||
const featureValue = props.featureValue;
|
const featureValue = props.featureValue;
|
||||||
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
|
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
|
||||||
@ -33,9 +34,7 @@ const ExperimentalFeatureRow = (props: {
|
|||||||
padding: 2,
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={translate("resources.experimental_features.action.toggle")}>
|
<Switch checked={checked} onChange={handleChange} />
|
||||||
<Switch checked={checked} onChange={handleChange} />
|
|
||||||
</Tooltip>
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
@ -75,7 +74,7 @@ export const ExperimentalFeaturesList = () => {
|
|||||||
const updateFeature = async (feature_name: string, feature_value: boolean) => {
|
const updateFeature = async (feature_name: string, feature_value: boolean) => {
|
||||||
const updatedFeatures = { ...features, [feature_name]: feature_value } as ExperimentalFeaturesModel;
|
const updatedFeatures = { ...features, [feature_name]: feature_value } as ExperimentalFeaturesModel;
|
||||||
setFeatures(updatedFeatures);
|
setFeatures(updatedFeatures);
|
||||||
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
|
await dataProvider.updateFeatures(record.id, updatedFeatures);
|
||||||
notify("ra.notification.updated", {
|
notify("ra.notification.updated", {
|
||||||
messageArgs: { smart_count: 1 },
|
messageArgs: { smart_count: 1 },
|
||||||
type: "success",
|
type: "success",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Avatar, Box, Link, Typography } from "@mui/material";
|
import { Avatar, Box, Link } from "@mui/material";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ const UserAccountData = () => {
|
|||||||
return (
|
return (
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{translate("ra.navigation.no_results", {
|
{translate("ra.navigation.no_results", {
|
||||||
resource: "Account Data",
|
name: "Account Data",
|
||||||
_: "No results found.",
|
_: "No results found.",
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -40,7 +40,7 @@ const UserAccountData = () => {
|
|||||||
<>
|
<>
|
||||||
<Stack direction="column" spacing={2} width="100%">
|
<Stack direction="column" spacing={2} width="100%">
|
||||||
<Typography variant="h6">{translate("resources.users.account_data.title")}</Typography>
|
<Typography variant="h6">{translate("resources.users.account_data.title")}</Typography>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1" component="div">
|
||||||
<Box>
|
<Box>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Stack, Typography } from "@mui/material";
|
import { Stack, Typography } from "@mui/material";
|
||||||
import { TextField } from "@mui/material";
|
import { TextField } from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
|
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
const RateLimitRow = ({
|
const RateLimitRow = ({
|
||||||
@ -10,8 +10,8 @@ const RateLimitRow = ({
|
|||||||
updateRateLimit,
|
updateRateLimit,
|
||||||
}: {
|
}: {
|
||||||
limit: string;
|
limit: string;
|
||||||
value: any;
|
value: object;
|
||||||
updateRateLimit: (limit: string, value: any) => void;
|
updateRateLimit: (limit: string, value: integer | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
@ -33,7 +33,6 @@ const RateLimitRow = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
id="outlined-number"
|
|
||||||
type="number"
|
type="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -54,8 +53,6 @@ const RateLimitRow = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserRateLimits = () => {
|
const UserRateLimits = () => {
|
||||||
const translate = useTranslate();
|
|
||||||
const notify = useNotify();
|
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
@ -79,7 +76,7 @@ const UserRateLimits = () => {
|
|||||||
fetchRateLimits();
|
fetchRateLimits();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateRateLimit = async (limit: string, value: any) => {
|
const updateRateLimit = async (limit: string, value: integer | null) => {
|
||||||
const updatedRateLimits = { ...rateLimits, [limit]: value };
|
const updatedRateLimits = { ...rateLimits, [limit]: value };
|
||||||
setRateLimits(updatedRateLimits);
|
setRateLimits(updatedRateLimits);
|
||||||
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
|
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
|
||||||
|
214
src/components/etke.cc/BillingPage.tsx
Normal file
214
src/components/etke.cc/BillingPage.tsx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import PaymentIcon from "@mui/icons-material/Payment";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Link,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useDataProvider, useNotify } from "react-admin";
|
||||||
|
|
||||||
|
import { useAppContext } from "../../Context";
|
||||||
|
import { SynapseDataProvider, Payment } from "../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const TruncatedUUID = ({ uuid }): React.ReactElement => {
|
||||||
|
const short = `${uuid.slice(0, 8)}...${uuid.slice(-6)}`;
|
||||||
|
const copyToClipboard = () => navigator.clipboard.writeText(uuid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={uuid}>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center" }}>
|
||||||
|
{short}
|
||||||
|
<IconButton size="small" onClick={copyToClipboard}>
|
||||||
|
<ContentCopyIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BillingPage = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider() as SynapseDataProvider;
|
||||||
|
const notify = useNotify();
|
||||||
|
const [paymentsData, setPaymentsData] = useState<Payment[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [failure, setFailure] = useState<string | null>(null);
|
||||||
|
const [downloadingInvoice, setDownloadingInvoice] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBillingData = async () => {
|
||||||
|
if (!etkeccAdmin) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await dataProvider.getPayments(etkeccAdmin);
|
||||||
|
setPaymentsData(response.payments);
|
||||||
|
setTotal(response.total);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching billing data:", error);
|
||||||
|
setFailure(error instanceof Error ? error.message : error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBillingData();
|
||||||
|
}, [etkeccAdmin, dataProvider, notify]);
|
||||||
|
|
||||||
|
const handleInvoiceDownload = async (transactionId: string) => {
|
||||||
|
if (!etkeccAdmin || downloadingInvoice) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setDownloadingInvoice(transactionId);
|
||||||
|
await dataProvider.getInvoice(etkeccAdmin, transactionId);
|
||||||
|
notify("Invoice download started", { type: "info" });
|
||||||
|
} catch (error) {
|
||||||
|
// Use the specific error message from the dataProvider
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Error downloading invoice";
|
||||||
|
notify(errorMessage, { type: "error" });
|
||||||
|
console.error("Error downloading invoice:", error);
|
||||||
|
} finally {
|
||||||
|
setDownloadingInvoice(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4">
|
||||||
|
<PaymentIcon sx={{ verticalAlign: "middle", mr: 1 }} /> Billing
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
View payments and generate invoices from here. More details about billing can be found{" "}
|
||||||
|
<Link href="https://etke.cc/help/extras/scheduler/#payments" target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
<br />
|
||||||
|
If you'd like to change your billing email, or add company details, please{" "}
|
||||||
|
<Link href="https://etke.cc/contacts/" target="_blank">
|
||||||
|
contact etke.cc support
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
{header}
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Typography>Loading billing information...</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failure) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
{header}
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Typography>
|
||||||
|
There was a problem loading your billing information.
|
||||||
|
<br />
|
||||||
|
This might be a temporary issue - please try again in a few minutes.
|
||||||
|
<br />
|
||||||
|
If it persists, contact{" "}
|
||||||
|
<Link href="https://etke.cc/contacts/" target="_blank">
|
||||||
|
etke.cc support team
|
||||||
|
</Link>{" "}
|
||||||
|
with the following error message:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
||||||
|
{failure}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
{header}
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="h5">Payment Summary</Typography>
|
||||||
|
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Typography variant="body1">Total Payments:</Typography>
|
||||||
|
<Chip label={total} color="primary" variant="outlined" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||||
|
Payment History
|
||||||
|
</Typography>
|
||||||
|
{paymentsData.length === 0 ? (
|
||||||
|
<Typography variant="body1">
|
||||||
|
No payments found. If you believe that's an error, please{" "}
|
||||||
|
<Link href="https://etke.cc/contacts/" target="_blank">
|
||||||
|
contact etke.cc support
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Transaction ID</TableCell>
|
||||||
|
<TableCell>Email</TableCell>
|
||||||
|
<TableCell>Type</TableCell>
|
||||||
|
<TableCell>Amount</TableCell>
|
||||||
|
<TableCell>Paid At</TableCell>
|
||||||
|
<TableCell>Download Invoice</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{paymentsData.map(payment => (
|
||||||
|
<TableRow key={payment.transaction_id}>
|
||||||
|
<TableCell>
|
||||||
|
<TruncatedUUID uuid={payment.transaction_id} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{payment.email}</TableCell>
|
||||||
|
<TableCell>{payment.is_subscription ? "Subscription" : "One-time"}</TableCell>
|
||||||
|
<TableCell>${payment.amount.toFixed(2)}</TableCell>
|
||||||
|
<TableCell>{new Date(payment.paid_at).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
onClick={() => handleInvoiceDownload(payment.transaction_id)}
|
||||||
|
disabled={downloadingInvoice === payment.transaction_id}
|
||||||
|
>
|
||||||
|
{downloadingInvoice === payment.transaction_id ? "Downloading..." : "Invoice"}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BillingPage;
|
@ -5,7 +5,7 @@ import { ServerProcessResponse } from "../../synapse/dataProvider";
|
|||||||
import { getTimeSince } from "../../utils/date";
|
import { getTimeSince } from "../../utils/date";
|
||||||
|
|
||||||
const CurrentlyRunningCommand = () => {
|
const CurrentlyRunningCommand = () => {
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
|
@ -65,3 +65,10 @@ On this page you can do the following:
|
|||||||
When you open [Server Actions page](#server-status-page), you will see the Server Commands panel.
|
When you open [Server Actions page](#server-status-page), you will see the Server Commands panel.
|
||||||
This panel contains all [the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click.
|
This panel contains all [the commands](https://etke.cc/help/extras/scheduler/#commands) you can run on your server in 1 click.
|
||||||
Once command is finished, you will get a notification about the result.
|
Once command is finished, you will get a notification about the result.
|
||||||
|
|
||||||
|
### Billing Page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click on the `Billing` sidebar menu item, you will be see the Billing page.
|
||||||
|
On this page you can see the list of successful payments and invoices.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import RestoreIcon from "@mui/icons-material/Restore";
|
import RestoreIcon from "@mui/icons-material/Restore";
|
||||||
import ScheduleIcon from "@mui/icons-material/Schedule";
|
import ScheduleIcon from "@mui/icons-material/Schedule";
|
||||||
import { Box, Typography, Link, Divider } from "@mui/material";
|
import { Box, Typography, Link } from "@mui/material";
|
||||||
import { Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
|
|
||||||
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||||
|
@ -23,6 +23,7 @@ import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider
|
|||||||
import { Icons } from "../../utils/icons";
|
import { Icons } from "../../utils/icons";
|
||||||
|
|
||||||
const renderIcon = (icon: string) => {
|
const renderIcon = (icon: string) => {
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||||
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
|
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
|
||||||
};
|
};
|
||||||
@ -80,6 +81,7 @@ const ServerCommandsPanel = () => {
|
|||||||
// Update server process status
|
// Update server process status
|
||||||
await updateServerProcessStatus(serverCommands[command]);
|
await updateServerProcessStatus(serverCommands[command]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error running command:", error);
|
||||||
setCommandIsRunning(false);
|
setCommandIsRunning(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@ const useServerNotifications = () => {
|
|||||||
notifications: [],
|
notifications: [],
|
||||||
success: false,
|
success: false,
|
||||||
});
|
});
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
|
@ -59,11 +59,11 @@ const useServerStatus = () => {
|
|||||||
host: "",
|
host: "",
|
||||||
results: [],
|
results: [],
|
||||||
});
|
});
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
const { command, locked_at } = serverProcess;
|
const { command } = serverProcess;
|
||||||
const { etkeccAdmin } = useAppContext();
|
const { etkeccAdmin } = useAppContext();
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
const isOkay = serverStatus.ok;
|
const isOkay = serverStatus.ok;
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import EngineeringIcon from "@mui/icons-material/Engineering";
|
import EngineeringIcon from "@mui/icons-material/Engineering";
|
||||||
import { Alert, Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
|
import { Box, Stack, Typography, Paper, Link, Chip, Divider, ChipProps } from "@mui/material";
|
||||||
import { useStore } from "ra-core";
|
import { useStore } from "ra-core";
|
||||||
|
|
||||||
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||||
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||||
import { getTimeSince } from "../../utils/date";
|
|
||||||
|
|
||||||
const StatusChip = ({
|
const StatusChip = ({
|
||||||
isOkay,
|
isOkay,
|
||||||
@ -40,17 +39,17 @@ const ServerComponentText = ({ text }: { text: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ServerStatusPage = () => {
|
const ServerStatusPage = () => {
|
||||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
const [serverStatus, _setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||||
ok: false,
|
ok: false,
|
||||||
success: false,
|
success: false,
|
||||||
host: "",
|
host: "",
|
||||||
results: [],
|
results: [],
|
||||||
});
|
});
|
||||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||||
command: "",
|
command: "",
|
||||||
locked_at: "",
|
locked_at: "",
|
||||||
});
|
});
|
||||||
const { command, locked_at } = serverProcess;
|
const { command } = serverProcess;
|
||||||
const successCheck = serverStatus.success;
|
const successCheck = serverStatus.success;
|
||||||
const isOkay = serverStatus.ok;
|
const isOkay = serverStatus.ok;
|
||||||
const host = serverStatus.host;
|
const host = serverStatus.host;
|
||||||
@ -104,7 +103,7 @@ const ServerStatusPage = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={2} direction="row">
|
<Stack spacing={2} direction="row">
|
||||||
{Object.keys(groupedResults).map((category, idx) => (
|
{Object.keys(groupedResults).map((category, _idx) => (
|
||||||
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
||||||
<Typography variant="h5" mb={1}>
|
<Typography variant="h5" mb={1}>
|
||||||
{category}
|
{category}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
|
||||||
return Object.entries(commands).map(([key, value]) => ({
|
|
||||||
id: key,
|
|
||||||
name: value.name,
|
|
||||||
description: value.description,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScheduledCommandCreate = () => {
|
|
||||||
const commandChoices = transformCommandsToChoices(serverCommands);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SimpleForm>
|
|
||||||
<SelectInput
|
|
||||||
source="command"
|
|
||||||
choices={commandChoices}
|
|
||||||
optionText={choice => `${choice.name} - ${choice.description}`}
|
|
||||||
/>
|
|
||||||
</SimpleForm>
|
|
||||||
);
|
|
||||||
};
|
|
@ -22,6 +22,7 @@ import { RecurringCommand } from "../../../../../synapse/dataProvider";
|
|||||||
import { useServerCommands } from "../../../hooks/useServerCommands";
|
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||||
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
|
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||||
return Object.entries(commands).map(([key, value]) => ({
|
return Object.entries(commands).map(([key, value]) => ({
|
||||||
id: key,
|
id: key,
|
||||||
@ -111,13 +112,11 @@ const RecurringCommandEdit = () => {
|
|||||||
delete submissionData.args;
|
delete submissionData.args;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
|
||||||
|
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
result = await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
|
await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
|
||||||
notify("recurring_commands.action.create_success", { type: "success" });
|
notify("recurring_commands.action.create_success", { type: "success" });
|
||||||
} else {
|
} else {
|
||||||
result = await dataProvider.updateRecurringCommand(etkeccAdmin, {
|
await dataProvider.updateRecurringCommand(etkeccAdmin, {
|
||||||
...submissionData,
|
...submissionData,
|
||||||
id: id,
|
id: id,
|
||||||
});
|
});
|
||||||
@ -129,6 +128,7 @@ const RecurringCommandEdit = () => {
|
|||||||
|
|
||||||
navigate("/server_actions");
|
navigate("/server_actions");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error saving recurring command:", error);
|
||||||
notify("recurring_commands.action.update_failure", { type: "error" });
|
notify("recurring_commands.action.update_failure", { type: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -21,7 +21,7 @@ const ListActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RecurringCommandsList = () => {
|
const RecurringCommandsList = () => {
|
||||||
const { data, isLoading, error } = useRecurringCommands();
|
const { data, isLoading } = useRecurringCommands();
|
||||||
|
|
||||||
const listContext = useList({
|
const listContext = useList({
|
||||||
resource: "recurring",
|
resource: "recurring",
|
||||||
@ -40,6 +40,7 @@ const RecurringCommandsList = () => {
|
|||||||
<Paper>
|
<Paper>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
bulkActionButtons={false}
|
bulkActionButtons={false}
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
rowClick={(id: Identifier, resource: string, record: any) => {
|
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
useDataProvider,
|
useDataProvider,
|
||||||
Loading,
|
Loading,
|
||||||
Button,
|
Button,
|
||||||
BooleanInput,
|
|
||||||
SelectInput,
|
SelectInput,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useWatch } from "react-hook-form";
|
import { useWatch } from "react-hook-form";
|
||||||
@ -23,6 +22,7 @@ import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
|||||||
import { useServerCommands } from "../../../hooks/useServerCommands";
|
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||||
return Object.entries(commands).map(([key, value]) => ({
|
return Object.entries(commands).map(([key, value]) => ({
|
||||||
id: key,
|
id: key,
|
||||||
@ -50,7 +50,7 @@ const ScheduledCommandEdit = () => {
|
|||||||
const isCreating = typeof id === "undefined";
|
const isCreating = typeof id === "undefined";
|
||||||
const [loading, setLoading] = useState(!isCreating);
|
const [loading, setLoading] = useState(!isCreating);
|
||||||
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
|
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
|
||||||
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
|
const { serverCommands } = useServerCommands();
|
||||||
const pageTitle = isCreating ? "Create Scheduled Command" : "Edit Scheduled Command";
|
const pageTitle = isCreating ? "Create Scheduled Command" : "Edit Scheduled Command";
|
||||||
|
|
||||||
const commandChoices = transformCommandsToChoices(serverCommands);
|
const commandChoices = transformCommandsToChoices(serverCommands);
|
||||||
@ -67,15 +67,12 @@ const ScheduledCommandEdit = () => {
|
|||||||
|
|
||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
try {
|
try {
|
||||||
let result;
|
|
||||||
|
|
||||||
data.scheduled_at = new Date(data.scheduled_at).toISOString();
|
data.scheduled_at = new Date(data.scheduled_at).toISOString();
|
||||||
|
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
result = await dataProvider.createScheduledCommand(etkeccAdmin, data);
|
await dataProvider.createScheduledCommand(etkeccAdmin, data);
|
||||||
notify("scheduled_commands.action.create_success", { type: "success" });
|
notify("scheduled_commands.action.create_success", { type: "success" });
|
||||||
} else {
|
} else {
|
||||||
result = await dataProvider.updateScheduledCommand(etkeccAdmin, {
|
await dataProvider.updateScheduledCommand(etkeccAdmin, {
|
||||||
...data,
|
...data,
|
||||||
id: id,
|
id: id,
|
||||||
});
|
});
|
||||||
@ -84,6 +81,7 @@ const ScheduledCommandEdit = () => {
|
|||||||
|
|
||||||
navigate("/server_actions");
|
navigate("/server_actions");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("Error saving scheduled command:", error);
|
||||||
notify("scheduled_commands.action.update_failure", { type: "error" });
|
notify("scheduled_commands.action.update_failure", { type: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,6 @@ import { useState, useEffect } from "react";
|
|||||||
import {
|
import {
|
||||||
Loading,
|
Loading,
|
||||||
Button,
|
Button,
|
||||||
useDataProvider,
|
|
||||||
useNotify,
|
|
||||||
SimpleShowLayout,
|
SimpleShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@ -15,7 +13,6 @@ import {
|
|||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import ScheduledDeleteButton from "./ScheduledDeleteButton";
|
import ScheduledDeleteButton from "./ScheduledDeleteButton";
|
||||||
import { useAppContext } from "../../../../../Context";
|
|
||||||
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import { Paper } from "@mui/material";
|
import { Paper } from "@mui/material";
|
||||||
import { Loading, Button, useNotify, useRefresh, useCreatePath, useRecordContext } from "react-admin";
|
import { Loading, Button } from "react-admin";
|
||||||
import { ResourceContextProvider, useList } from "react-admin";
|
import { ResourceContextProvider, useList } from "react-admin";
|
||||||
import { ListContextProvider, TextField } from "react-admin";
|
import { ListContextProvider, TextField } from "react-admin";
|
||||||
import { Datagrid } from "react-admin";
|
import { Datagrid } from "react-admin";
|
||||||
import { BooleanField, DateField, TopToolbar } from "react-admin";
|
import { BooleanField, DateField, TopToolbar } from "react-admin";
|
||||||
import { useDataProvider } from "react-admin";
|
|
||||||
import { Identifier } from "react-admin";
|
import { Identifier } from "react-admin";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useAppContext } from "../../../../../Context";
|
|
||||||
import { DATE_FORMAT } from "../../../../../utils/date";
|
import { DATE_FORMAT } from "../../../../../utils/date";
|
||||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||||
const ListActions = () => {
|
const ListActions = () => {
|
||||||
@ -27,7 +25,7 @@ const ListActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ScheduledCommandsList = () => {
|
const ScheduledCommandsList = () => {
|
||||||
const { data, isLoading, error } = useScheduledCommands();
|
const { data, isLoading } = useScheduledCommands();
|
||||||
|
|
||||||
const listContext = useList({
|
const listContext = useList({
|
||||||
resource: "scheduled",
|
resource: "scheduled",
|
||||||
@ -46,6 +44,7 @@ const ScheduledCommandsList = () => {
|
|||||||
<Paper>
|
<Paper>
|
||||||
<Datagrid
|
<Datagrid
|
||||||
bulkActionButtons={false}
|
bulkActionButtons={false}
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
rowClick={(id: Identifier, resource: string, record: any) => {
|
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -7,16 +7,7 @@ import DownloadingIcon from "@mui/icons-material/Downloading";
|
|||||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import {
|
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
|
||||||
Grid2 as Grid,
|
|
||||||
Box,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
|
||||||
Tooltip,
|
|
||||||
Link,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
@ -50,14 +41,10 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
|
|
||||||
const DeleteMediaToolbar = (props: ToolbarProps) => (
|
const DeleteMediaToolbar = (props: ToolbarProps) => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<Tooltip title={translate("delete_media.helper.send")}>
|
<SaveButton label="delete_media.action.send" icon={<DeleteSweepIcon />} />
|
||||||
<SaveButton label="delete_media.action.send" icon={<DeleteSweepIcon />} />
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
</Tooltip>
|
<IconCancel />
|
||||||
<Tooltip title={translate("ra.action.cancel")}>
|
</Button>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
|
||||||
<IconCancel />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -67,20 +54,9 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
<Tooltip title={translate("delete_media.helper.before_ts")}>
|
<DateTimeInput source="before_ts" label="delete_media.fields.before_ts" defaultValue={0} parse={dateParser} />
|
||||||
<DateTimeInput
|
<NumberInput source="size_gt" label="delete_media.fields.size_gt" defaultValue={0} min={0} step={1024} />
|
||||||
source="before_ts"
|
<BooleanInput source="keep_profiles" label="delete_media.fields.keep_profiles" defaultValue={true} />
|
||||||
label="delete_media.fields.before_ts"
|
|
||||||
defaultValue={0}
|
|
||||||
parse={dateParser}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={translate("delete_media.helper.size_gt")}>
|
|
||||||
<NumberInput source="size_gt" label="delete_media.fields.size_gt" defaultValue={0} min={0} step={1024} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={translate("delete_media.helper.keep_profiles")}>
|
|
||||||
<BooleanInput source="keep_profiles" label="delete_media.fields.keep_profiles" defaultValue={true} />
|
|
||||||
</Tooltip>
|
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -91,7 +67,6 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const translate = useTranslate();
|
|
||||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
const { mutate: deleteMedia, isPending } = useMutation({
|
const { mutate: deleteMedia, isPending } = useMutation({
|
||||||
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||||
@ -111,26 +86,24 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={translate("delete_media.helper.send")}>
|
<Button
|
||||||
<Button
|
{...props}
|
||||||
{...props}
|
label="delete_media.action.send"
|
||||||
label="delete_media.action.send"
|
onClick={openDialog}
|
||||||
onClick={openDialog}
|
disabled={isPending}
|
||||||
disabled={isPending}
|
sx={{
|
||||||
sx={{
|
color: theme.palette.error.main,
|
||||||
color: theme.palette.error.main,
|
"&:hover": {
|
||||||
"&:hover": {
|
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||||
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
// Reset on mouse devices
|
||||||
// Reset on mouse devices
|
"@media (hover: none)": {
|
||||||
"@media (hover: none)": {
|
backgroundColor: "transparent",
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
<DeleteSweepIcon />
|
>
|
||||||
</Button>
|
<DeleteSweepIcon />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<DeleteMediaDialog open={open} onClose={closeDialog} onSubmit={deleteMedia} />
|
<DeleteMediaDialog open={open} onClose={closeDialog} onSubmit={deleteMedia} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -141,14 +114,10 @@ const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
|
|
||||||
const PurgeRemoteMediaToolbar = (props: ToolbarProps) => (
|
const PurgeRemoteMediaToolbar = (props: ToolbarProps) => (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<Tooltip title={translate("purge_remote_media.helper.send")}>
|
<SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} />
|
||||||
<SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} />
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
</Tooltip>
|
<IconCancel />
|
||||||
<Tooltip title={translate("ra.action.cancel")}>
|
</Button>
|
||||||
<Button label="ra.action.cancel" onClick={onClose}>
|
|
||||||
<IconCancel />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -171,10 +140,8 @@ const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
||||||
const theme = useTheme();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const translate = useTranslate();
|
|
||||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
const { mutate: purgeRemoteMedia, isPending } = useMutation({
|
const { mutate: purgeRemoteMedia, isPending } = useMutation({
|
||||||
mutationFn: (values: DeleteMediaParams) => dataProvider.purgeRemoteMedia(values),
|
mutationFn: (values: DeleteMediaParams) => dataProvider.purgeRemoteMedia(values),
|
||||||
@ -194,24 +161,22 @@ export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={translate("purge_remote_media.helper.send")}>
|
<Button
|
||||||
<Button
|
{...props}
|
||||||
{...props}
|
label="purge_remote_media.action.send"
|
||||||
label="purge_remote_media.action.send"
|
onClick={openDialog}
|
||||||
onClick={openDialog}
|
disabled={isPending}
|
||||||
disabled={isPending}
|
sx={{
|
||||||
sx={{
|
"&:hover": {
|
||||||
"&:hover": {
|
// Reset on mouse devices
|
||||||
// Reset on mouse devices
|
"@media (hover: none)": {
|
||||||
"@media (hover: none)": {
|
backgroundColor: "transparent",
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
<DeleteSweepIcon />
|
>
|
||||||
</Button>
|
<DeleteSweepIcon />
|
||||||
</Tooltip>
|
</Button>
|
||||||
<PurgeRemoteMediaDialog open={open} onClose={closeDialog} onSubmit={purgeRemoteMedia} />
|
<PurgeRemoteMediaDialog open={open} onClose={closeDialog} onSubmit={purgeRemoteMedia} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -487,7 +452,6 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MediaIDField = ({ source }) => {
|
export const MediaIDField = ({ source }) => {
|
||||||
const translate = useTranslate();
|
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
@ -510,15 +474,10 @@ export const MediaIDField = ({ source }) => {
|
|||||||
mxcURL = `mxc://${homeserver}/${mediaID}`;
|
mxcURL = `mxc://${homeserver}/${mediaID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type} />;
|
||||||
<Tooltip title={translate("resources.users_media.action.open")}>
|
|
||||||
<ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReportMediaContent = ({ source }) => {
|
export const ReportMediaContent = ({ source }) => {
|
||||||
const translate = useTranslate();
|
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
@ -534,9 +493,5 @@ export const ReportMediaContent = ({ source }) => {
|
|||||||
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
|
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type} />;
|
||||||
<Tooltip title={translate("resources.users_media.action.open")}>
|
|
||||||
<ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { CardContent, CardHeader, Container } from "@mui/material";
|
|||||||
import { useTranslate } from "ra-core";
|
import { useTranslate } from "ra-core";
|
||||||
import { ChangeEventHandler } from "react";
|
import { ChangeEventHandler } from "react";
|
||||||
|
|
||||||
import { ParsedStats, Progress } from "./types";
|
import { ImportResult, ParsedStats, Progress } from "./types";
|
||||||
|
|
||||||
const TranslatableOption = ({ value, text }: { value: string; text: string }) => {
|
const TranslatableOption = ({ value, text }: { value: string; text: string }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -18,7 +18,7 @@ const ConflictModeCard = ({
|
|||||||
progress,
|
progress,
|
||||||
}: {
|
}: {
|
||||||
stats: ParsedStats | null;
|
stats: ParsedStats | null;
|
||||||
importResults: any;
|
importResults: ImportResult | null;
|
||||||
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
||||||
conflictMode: string;
|
conflictMode: string;
|
||||||
progress: Progress;
|
progress: Progress;
|
||||||
|
@ -5,7 +5,7 @@ import { Checkbox } from "@mui/material";
|
|||||||
import { useTranslate } from "ra-core";
|
import { useTranslate } from "ra-core";
|
||||||
import { ChangeEventHandler } from "react";
|
import { ChangeEventHandler } from "react";
|
||||||
|
|
||||||
import { ParsedStats, Progress } from "./types";
|
import { ImportResult, ParsedStats, Progress } from "./types";
|
||||||
|
|
||||||
const StatsCard = ({
|
const StatsCard = ({
|
||||||
stats,
|
stats,
|
||||||
@ -18,7 +18,7 @@ const StatsCard = ({
|
|||||||
}: {
|
}: {
|
||||||
stats: ParsedStats | null;
|
stats: ParsedStats | null;
|
||||||
progress: Progress;
|
progress: Progress;
|
||||||
importResults: any;
|
importResults: ImportResult | null;
|
||||||
useridMode: string;
|
useridMode: string;
|
||||||
passwordMode: boolean;
|
passwordMode: boolean;
|
||||||
onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
||||||
|
@ -2,14 +2,14 @@ import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } fr
|
|||||||
import { useTranslate } from "ra-core";
|
import { useTranslate } from "ra-core";
|
||||||
import { ChangeEventHandler } from "react";
|
import { ChangeEventHandler } from "react";
|
||||||
|
|
||||||
import { Progress } from "./types";
|
import { ImportResult, Progress } from "./types";
|
||||||
|
|
||||||
const UploadCard = ({
|
const UploadCard = ({
|
||||||
importResults,
|
importResults,
|
||||||
onFileChange,
|
onFileChange,
|
||||||
progress,
|
progress,
|
||||||
}: {
|
}: {
|
||||||
importResults: any;
|
importResults: ImportResult | null;
|
||||||
onFileChange: ChangeEventHandler<HTMLInputElement>;
|
onFileChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
progress: Progress;
|
progress: Progress;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -273,7 +273,7 @@ const useImportFile = () => {
|
|||||||
let retries = 0;
|
let retries = 0;
|
||||||
const submitRecord = async (recordData: ImportLine) => {
|
const submitRecord = async (recordData: ImportLine) => {
|
||||||
try {
|
try {
|
||||||
const response = await dataProvider.getOne("users", { id: recordData.id });
|
await dataProvider.getOne("users", { id: recordData.id });
|
||||||
|
|
||||||
if (LOGGING) console.log("already existed");
|
if (LOGGING) console.log("already existed");
|
||||||
|
|
||||||
|
@ -204,11 +204,14 @@ const de: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
||||||
|
password_required_for_reactivation: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
|
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
|
||||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
suspend:
|
suspend:
|
||||||
"Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.",
|
"Ein gesperrter Benutzer kann sich nicht mehr anmelden und wird in den schreibgeschützten Modus versetzt.",
|
||||||
erase: "DSGVO konformes Löschen der Benutzerdaten.",
|
erase: "DSGVO konformes Löschen der Benutzerdaten.",
|
||||||
|
admin: "Ein Serveradministrator hat volle Kontrolle über den Server und seine Benutzer.",
|
||||||
|
lock: "Verhindert, dass der Benutzer den Server nutzen kann. Dies ist eine nicht-destruktive Aktion, die rückgängig gemacht werden kann.",
|
||||||
erase_text:
|
erase_text:
|
||||||
"Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
|
"Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
|
||||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
|
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 Michael Albert
|
||||||
|
// SPDX-FileCopyrightText: 2020 - 2024 Manuel Stahl
|
||||||
|
// SPDX-FileCopyrightText: 2021 Dirk Klimpel
|
||||||
|
// SPDX-FileCopyrightText: 2023 Przemysław Romanik
|
||||||
|
// SPDX-FileCopyrightText: 2024 Alexander Tumin
|
||||||
|
// SPDX-FileCopyrightText: 2024 - 2025 Borislav Pantaleev
|
||||||
|
// SPDX-FileCopyrightText: 2024 - 2025 Nikita Chernyi
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import englishMessages from "ra-language-english";
|
import englishMessages from "ra-language-english";
|
||||||
|
|
||||||
import { SynapseTranslationMessages } from ".";
|
import { SynapseTranslationMessages } from ".";
|
||||||
@ -171,10 +181,13 @@ const en: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Changing password will log user out of all sessions.",
|
password: "Changing password will log user out of all sessions.",
|
||||||
|
password_required_for_reactivation: "You must provide a password to re-activate an account.",
|
||||||
create_password: "Generate a strong and secure password using the button below.",
|
create_password: "Generate a strong and secure password using the button below.",
|
||||||
|
lock: "Prevent the user from usefully using their account. This is a non-destructive action that can be reversed.",
|
||||||
deactivate: "You must provide a password to re-activate an account.",
|
deactivate: "You must provide a password to re-activate an account.",
|
||||||
suspend: "Suspending user means they are put into a read-only mode.",
|
suspend: "Suspending user means they are put into a read-only mode.",
|
||||||
erase: "Mark the user as GDPR-erased",
|
erase: "In addition to deactivating the user, mark the user as GDPR-erased.",
|
||||||
|
admin: "A server administrator has full control over the server and its users.",
|
||||||
erase_text:
|
erase_text:
|
||||||
"This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
|
"This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
|
||||||
erase_admin_error: "Deleting own user is not allowed.",
|
erase_admin_error: "Deleting own user is not allowed.",
|
||||||
|
@ -162,6 +162,9 @@ const fa: SynapseTranslationMessages = {
|
|||||||
user_type: "نوع کاربر",
|
user_type: "نوع کاربر",
|
||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
|
password_required_for_reactivation: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
|
admin: "مدیر سرور دارای کنترل کامل بر روی سرور و کاربران آن است.",
|
||||||
|
lock: "ممنوعیت استفاده از سرور توسط کاربر. این یک عملیات غیر مخرب است که می تواند برگردانده شود.",
|
||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||||
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
|
@ -170,10 +170,13 @@ const fr: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
|
||||||
|
password_required_for_reactivation: "Vous devez fournir un mot de passe pour réactiver le compte.",
|
||||||
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
|
||||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||||
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
|
suspend: "L'utilisateur sera suspendu jusqu'à ce que vous le réactiviez.",
|
||||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||||
|
admin: "Un administrateur de serveur a un contrôle total sur le serveur et ses utilisateurs.",
|
||||||
|
lock: "Empêche l'utilisateur d'utiliser le serveur. C'est une action non destructive qui peut être annulée.",
|
||||||
erase_text:
|
erase_text:
|
||||||
"Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
|
"Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
|
||||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||||
|
3
src/i18n/index.d.ts
vendored
3
src/i18n/index.d.ts
vendored
@ -161,9 +161,12 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
};
|
};
|
||||||
helper: {
|
helper: {
|
||||||
password: string;
|
password: string;
|
||||||
|
password_required_for_reactivation: string;
|
||||||
create_password: string;
|
create_password: string;
|
||||||
|
lock: string;
|
||||||
deactivate: string;
|
deactivate: string;
|
||||||
suspend: string;
|
suspend: string;
|
||||||
|
admin: string;
|
||||||
erase: string;
|
erase: string;
|
||||||
erase_text: string;
|
erase_text: string;
|
||||||
erase_admin_error: string;
|
erase_admin_error: string;
|
||||||
|
@ -163,10 +163,13 @@ const it: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
||||||
|
password_required_for_reactivation: "Devi fornire una password per riattivare l'account.",
|
||||||
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
create_password: "Genera una password forte e sicura utilizzando il pulsante sottostante.",
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||||
suspend: "Sospendi l'utente",
|
suspend: "Sospendi l'utente",
|
||||||
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
||||||
|
admin: "Un amministratore del server ha controllo totale sul server e sui suoi utenti.",
|
||||||
|
lock: "Impedisce all'utente di utilizzare il server. Questa è un'azione non distruttiva che può essere annullata.",
|
||||||
erase_text:
|
erase_text:
|
||||||
"Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
|
"Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
|
||||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||||
|
519
src/i18n/ja.ts
Normal file
519
src/i18n/ja.ts
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2020 Michael Albert
|
||||||
|
// SPDX-FileCopyrightText: 2020 - 2024 Manuel Stahl
|
||||||
|
// SPDX-FileCopyrightText: 2021 Dirk Klimpel
|
||||||
|
// SPDX-FileCopyrightText: 2023 Przemysław Romanik
|
||||||
|
// SPDX-FileCopyrightText: 2024 Alexander Tumin
|
||||||
|
// SPDX-FileCopyrightText: 2024 - 2025 Borislav Pantaleev
|
||||||
|
// SPDX-FileCopyrightText: 2024 - 2025 Nikita Chernyi
|
||||||
|
// SPDX-FileCopyrightText: 2025 Suguru Hirahara
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import japaneseMessages from "@bicstone/ra-language-japanese";
|
||||||
|
|
||||||
|
import { SynapseTranslationMessages } from ".";
|
||||||
|
|
||||||
|
const ja: SynapseTranslationMessages = {
|
||||||
|
...japaneseMessages,
|
||||||
|
synapseadmin: {
|
||||||
|
auth: {
|
||||||
|
base_url: "ホームサーバーのURL",
|
||||||
|
welcome: "Synapse Adminにようこそ",
|
||||||
|
server_version: "Synapseのバージョン",
|
||||||
|
supports_specs: "次のMatrixのスペックをサポートしています",
|
||||||
|
username_error: "有効なユーザーIDを入力してください。形式は「@user:domain」です。",
|
||||||
|
protocol_error: "URLの先頭には「http://」または「https://」を置いてください",
|
||||||
|
url_error: "正しいMatrixのサーバーのURLではありません",
|
||||||
|
sso_sign_in: "シングルサインオン",
|
||||||
|
credentials: "認証情報",
|
||||||
|
access_token: "アクセストークン",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "既存のMatrixアクセストークンが使われています。",
|
||||||
|
content:
|
||||||
|
"このセッションを破棄しますか? このセッションは、Matrixのクライアントなどで使われている可能性があります。または、管理パネルからログアウトしますか?",
|
||||||
|
confirm: "破棄する",
|
||||||
|
cancel: "管理パネルからログアウト",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
invalid_user_id: "ホームサーバーが指定されていないMatrixのユーザーIDです。",
|
||||||
|
tabs: {
|
||||||
|
sso: "シングルサインオン",
|
||||||
|
experimental: "実験的",
|
||||||
|
limits: "レート制限",
|
||||||
|
account_data: "アカウントのデータ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
details: "ルームの詳細",
|
||||||
|
tabs: {
|
||||||
|
basic: "基本情報",
|
||||||
|
members: "メンバー",
|
||||||
|
detail: "詳細",
|
||||||
|
permission: "権限",
|
||||||
|
media: "メディア",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reports: { tabs: { basic: "基本情報", detail: "詳細" } },
|
||||||
|
},
|
||||||
|
import_users: {
|
||||||
|
error: {
|
||||||
|
at_entry: "エントリー %{entry}: %{message}",
|
||||||
|
error: "エラー",
|
||||||
|
required_field: "必須のフィールド「%{field}」がありません",
|
||||||
|
invalid_value:
|
||||||
|
"%{row}行目に不正な値があります。「%{field}」のフィールドには「true」または「false」を指定してください",
|
||||||
|
unreasonably_big: "ファイルは%{size}メガバイトで大きすぎるため、読み込みを行いませんでした",
|
||||||
|
already_in_progress: "インポートを実行しています",
|
||||||
|
id_exits: "ID %{id} は既に存在しています",
|
||||||
|
},
|
||||||
|
title: "CSVでユーザーをインポート",
|
||||||
|
goToPdf: "Go to PDF",
|
||||||
|
cards: {
|
||||||
|
importstats: {
|
||||||
|
header: "インポートするユーザー",
|
||||||
|
users_total: "CSVファイルの%{smart_count}人のユーザー",
|
||||||
|
guest_count: "%{smart_count}人のゲスト",
|
||||||
|
admin_count: "%{smart_count}人の管理者",
|
||||||
|
},
|
||||||
|
conflicts: {
|
||||||
|
header: "競合を処理する方針",
|
||||||
|
mode: {
|
||||||
|
stop: "競合の発生時に停止",
|
||||||
|
skip: "エラーを表示して競合をスキップ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ids: {
|
||||||
|
header: "ID",
|
||||||
|
all_ids_present: "全てのエントリーにIDsがあります",
|
||||||
|
count_ids_present: "%{smart_count}個のエントリーにIDがあります",
|
||||||
|
mode: {
|
||||||
|
ignore: "CSVファイルのIDを無視し、新しいIDを作成",
|
||||||
|
update: "既存のレコードを更新",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
passwords: {
|
||||||
|
header: "パスワード",
|
||||||
|
all_passwords_present: "全てのエントリーにパスワードがあります",
|
||||||
|
count_passwords_present: "%{smart_count}個のエントリーにパスワードがあります",
|
||||||
|
use_passwords: "CSVファイルのパスワードを使用",
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
header: "CSVファイルを送信",
|
||||||
|
explanation:
|
||||||
|
"作成またはアップデートするユーザーをコンマで区切って入力したファイルをアップロードできます。ファイルには「id」と「displayname」のフィールドを含めてください。参照用のファイルは以下からダウンロードできます。",
|
||||||
|
},
|
||||||
|
startImport: {
|
||||||
|
simulate_only: "シミュレーション",
|
||||||
|
run_import: "インポート",
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
header: "インポートの結果",
|
||||||
|
total: "合計%{smart_count}個のエントリー",
|
||||||
|
successful: "%{smart_count}個のエントリーをインポートしました",
|
||||||
|
skipped: "%{smart_count}個のエントリーをスキップしました",
|
||||||
|
download_skipped: "スキップしたエントリーをダウンロード",
|
||||||
|
with_error: "%{smart_count}個のエントリーでエラーが発生しました",
|
||||||
|
simulated_only: "シミュレーションのみ実行",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete_media: {
|
||||||
|
name: "メディアファイル",
|
||||||
|
fields: {
|
||||||
|
before_ts: "最終アクセス日時がこれより以前のもの",
|
||||||
|
size_gt: "サイズがこれより大きいもの(バイト)",
|
||||||
|
keep_profiles: "プロフィールの画像は削除しない",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "メディアファイルを削除",
|
||||||
|
send_success: "リクエストを送信しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "このAPIを使うとサーバーからローカルメディアファイルを削除できます。削除できるファイルは、ローカルのサムネイルファイルと、ダウンロードしたメディアファイルのコピーも含みます。外部のメディアリポジトリーにアップロードされたメディアファイルは削除できません。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "リモートのメディアファイル",
|
||||||
|
fields: {
|
||||||
|
before_ts: "最終アクセス日時がこれより以前のもの",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "リモートのメディアファイルを削除",
|
||||||
|
send_success: "削除のリクエストを送信しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "このAPIを使うとサーバーからリモートメディアファイルのキャッシュを削除できます。削除できるファイルは、ローカルのサムネイルファイルと、ダウンロードしたメディアファイルのコピーも含みます。サーバーのメディアリポジトリーにアップロードされたメディアファイルは削除できません。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
users: {
|
||||||
|
name: "ユーザー",
|
||||||
|
email: "メールアドレス",
|
||||||
|
msisdn: "電話番号",
|
||||||
|
threepid: "メールアドレスまたは電話番号",
|
||||||
|
fields: {
|
||||||
|
avatar: "アバター",
|
||||||
|
id: "ユーザーID",
|
||||||
|
name: "名前",
|
||||||
|
is_guest: "ゲスト",
|
||||||
|
admin: "サーバーの管理者",
|
||||||
|
locked: "ロック",
|
||||||
|
suspended: "停止",
|
||||||
|
deactivated: "無効化",
|
||||||
|
erased: "消去",
|
||||||
|
guests: "ゲストを表示",
|
||||||
|
show_deactivated: "無効化されたユーザーを表示",
|
||||||
|
show_locked: "ロックされたユーザーを表示",
|
||||||
|
show_suspended: "停止されたユーザーを表示",
|
||||||
|
user_id: "ユーザーを検索",
|
||||||
|
displayname: "表示名",
|
||||||
|
password: "パスワード",
|
||||||
|
avatar_url: "アバターのURL",
|
||||||
|
avatar_src: "アバター",
|
||||||
|
medium: "Medium",
|
||||||
|
threepids: "サードパーティーのID",
|
||||||
|
address: "アドレス",
|
||||||
|
creation_ts_ms: "作成日時",
|
||||||
|
consent_version: "同意のバージョン",
|
||||||
|
auth_provider: "プロバイダー",
|
||||||
|
user_type: "ユーザーの種類",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
password: "パスワードを変更すると、全てのセッションからログアウトします。",
|
||||||
|
password_required_for_reactivation: "アカウントを再度有効にするにはパスワードを設定する必要があります",
|
||||||
|
create_password: "以下のボタンで強力なパスワードを生成できます。",
|
||||||
|
lock: "ユーザーにアカウントを使用できないよう設定。これは後から取り消せます。",
|
||||||
|
deactivate: "アカウントを再度有効にするにはパスワードを設定する必要があります。",
|
||||||
|
suspend: "ユーザーを停止すると、ユーザーは読み込み限定のモードに設定されます。",
|
||||||
|
erase: "ユーザーをGDPRに準拠した形で消去",
|
||||||
|
admin: "サーバーの管理者には、サーバーとユーザーに対する完全なコントロールの権利が与えられています。",
|
||||||
|
erase_text:
|
||||||
|
"ユーザーが送信したメッセージは、メッセージが送信された時点にルームに参加していたユーザーは今後もこれを閲覧できますが、その後で参加したユーザーには表示されません。",
|
||||||
|
erase_admin_error: "自分自身のユーザーは削除できません。",
|
||||||
|
modify_managed_user_error: "システムが管理しているユーザーは変更できません。",
|
||||||
|
username_available: "ユーザー名は利用できます",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: "ユーザーのデータを消去",
|
||||||
|
erase_avatar: "アバターを消去",
|
||||||
|
delete_media: "このユーザーがアップロードしたメディアファイルを削除",
|
||||||
|
redact_events: "このユーザーが送信したイベントを削除",
|
||||||
|
generate_password: "パスワードを生成",
|
||||||
|
overwrite_title: "注意!",
|
||||||
|
overwrite_content: "このユーザー名はすでに取得されています。既存のユーザーを上書きしてもよろしいですか?",
|
||||||
|
overwrite_cancel: "キャンセル",
|
||||||
|
overwrite_confirm: "上書きする",
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
you: "あなた",
|
||||||
|
bot: "ボット",
|
||||||
|
admin: "管理者",
|
||||||
|
support: "サポート",
|
||||||
|
regular: "一般ユーザー",
|
||||||
|
system_managed: "システム管理",
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "毎秒のメッセージ数",
|
||||||
|
messages_per_second_text: "毎秒ごとに実行できるアクションの数。",
|
||||||
|
burst_count: "バースト数",
|
||||||
|
burst_count_text: "制限が実行されるまで行えるアクションの数。",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "アカウントのデータ",
|
||||||
|
global: "グローバル",
|
||||||
|
rooms: "ルーム",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
name: "ルーム",
|
||||||
|
fields: {
|
||||||
|
room_id: "ルームのID",
|
||||||
|
name: "名称",
|
||||||
|
canonical_alias: "エイリアス",
|
||||||
|
joined_members: "メンバー",
|
||||||
|
joined_local_members: "ローカルのメンバー",
|
||||||
|
joined_local_devices: "ローカルの端末",
|
||||||
|
state_events: "ステートイベント / 複雑さ",
|
||||||
|
version: "バージョン",
|
||||||
|
is_encrypted: "暗号化",
|
||||||
|
encryption: "暗号化",
|
||||||
|
federatable: "フェデレーションに対応",
|
||||||
|
public: "ルームディレクトリーに表示",
|
||||||
|
creator: "作成者",
|
||||||
|
join_rules: "参加のルール",
|
||||||
|
guest_access: "ゲストによるアクセス",
|
||||||
|
history_visibility: "履歴の見え方",
|
||||||
|
topic: "トピック",
|
||||||
|
avatar: "アバター",
|
||||||
|
actions: "アクション",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
forward_extremities:
|
||||||
|
"転送末端(forward extremities)は、ルーム内の有向非巡回グラフ(DAG)の終端にあるイベント、つまり、子をもたないイベントのことをいいます。これが多ければ多いほど、Synapseが実行しなければならないステート解決(これは負荷の大きい作業です)の数も多くなります。Synapseには、ルーム内に存在する末端の数を減らす仕組みが備わっていますが、バグによりそれが機能しない場合があります。もしルームに10個以上の転送末端がある場合は、どのルームがそれを引き起こしているかを確認して #1760 で参照されているSQLクエリーで転送末端を削除することを検討してみてください。",
|
||||||
|
},
|
||||||
|
enums: {
|
||||||
|
join_rules: {
|
||||||
|
public: "公開",
|
||||||
|
knock: "ノック",
|
||||||
|
invite: "招待",
|
||||||
|
private: "非公開",
|
||||||
|
},
|
||||||
|
guest_access: {
|
||||||
|
can_join: "ゲスト参加可",
|
||||||
|
forbidden: "ゲスト参加不可",
|
||||||
|
},
|
||||||
|
history_visibility: {
|
||||||
|
invited: "招待以後",
|
||||||
|
joined: "参加以後",
|
||||||
|
shared: "共有以後",
|
||||||
|
world_readable: "制限なし",
|
||||||
|
},
|
||||||
|
unencrypted: "非暗号化",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "ルームの削除",
|
||||||
|
content:
|
||||||
|
"ルームを削除してよろしいですか? これは取り消せません。ルームのメッセージとメディアファイルはサーバーから削除されます!",
|
||||||
|
fields: {
|
||||||
|
block: "ユーザーがルームに参加できないように設定",
|
||||||
|
},
|
||||||
|
success: "ルームを削除しました。",
|
||||||
|
failure: "ルームを削除できませんでした。",
|
||||||
|
},
|
||||||
|
make_admin: {
|
||||||
|
assign_admin: "管理者を任命",
|
||||||
|
title: "%{roomName}のルームの管理者を任命",
|
||||||
|
confirm: "管理者にする",
|
||||||
|
content:
|
||||||
|
"管理者に任命するユーザーのMXIDを入力してください。\n注意:これが機能するには、ルームには管理者となるローカルメンバーが最低1人以上いる必要があります。",
|
||||||
|
success: "ユーザーをルームの管理者に設定しました。",
|
||||||
|
failure: "ユーザーをルームの管理者に設定できませんでした。%{errMsg}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reports: {
|
||||||
|
name: "報告されたイベント",
|
||||||
|
fields: {
|
||||||
|
id: "ID",
|
||||||
|
received_ts: "報告日時",
|
||||||
|
user_id: "報告者",
|
||||||
|
name: "ルーム名",
|
||||||
|
score: "点数",
|
||||||
|
reason: "理由",
|
||||||
|
event_id: "イベントのID",
|
||||||
|
event_json: {
|
||||||
|
origin: "送信元のサーバー",
|
||||||
|
origin_server_ts: "送信日時",
|
||||||
|
type: "イベントの種類",
|
||||||
|
content: {
|
||||||
|
msgtype: "内容の種類",
|
||||||
|
body: "内容",
|
||||||
|
format: "形式",
|
||||||
|
formatted_body: "フォーマット済の内容",
|
||||||
|
algorithm: "アルゴリズム",
|
||||||
|
url: "URL",
|
||||||
|
info: {
|
||||||
|
mimetype: "種類",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "報告されたイベントを削除",
|
||||||
|
content: "報告されたイベントを削除してよろしいですか?これは取り消せません。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connections: {
|
||||||
|
name: "接続",
|
||||||
|
fields: {
|
||||||
|
last_seen: "日時",
|
||||||
|
ip: "IPアドレス",
|
||||||
|
user_agent: "ユーザーエージェント",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
devices: {
|
||||||
|
name: "端末",
|
||||||
|
fields: {
|
||||||
|
device_id: "端末のID",
|
||||||
|
display_name: "端末の名称",
|
||||||
|
last_seen_ts: "タイムスタンプ",
|
||||||
|
last_seen_ip: "IPアドレス",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "%{id}を削除",
|
||||||
|
content: "「%{name}」を削除してよろしいですか?",
|
||||||
|
success: "端末を削除しました。",
|
||||||
|
failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users_media: {
|
||||||
|
name: "メディアファイル",
|
||||||
|
fields: {
|
||||||
|
media_id: "メディアのID",
|
||||||
|
media_length: "ファイルの大きさ(バイト数)",
|
||||||
|
media_type: "種類",
|
||||||
|
upload_name: "ファイル名",
|
||||||
|
quarantined_by: "検疫の実行者",
|
||||||
|
safe_from_quarantine: "検疫で保護",
|
||||||
|
created_ts: "作成日時",
|
||||||
|
last_access_ts: "最終アクセス",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
open: "メディアファイルを新しいウィンドウで開く",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
protect_media: {
|
||||||
|
action: {
|
||||||
|
create: "未保護。保護を実行",
|
||||||
|
delete: "保護済。保護を削除",
|
||||||
|
none: "検疫済",
|
||||||
|
send_success: "保護に関する状態を変更しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quarantine_media: {
|
||||||
|
action: {
|
||||||
|
name: "検疫",
|
||||||
|
create: "検疫に追加",
|
||||||
|
delete: "検疫に追加されています。検疫から取り出す",
|
||||||
|
none: "検疫によって保護されています",
|
||||||
|
send_success: "検疫に関する状態を変更しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pushers: {
|
||||||
|
name: "プッシュ",
|
||||||
|
fields: {
|
||||||
|
app: "アプリケーション",
|
||||||
|
app_display_name: "アプリケーションの名称",
|
||||||
|
app_id: "アプリケーションのID",
|
||||||
|
device_display_name: "端末の名称",
|
||||||
|
kind: "種類",
|
||||||
|
lang: "言語",
|
||||||
|
profile_tag: "プロフィールのタグ",
|
||||||
|
pushkey: "プッシュ鍵",
|
||||||
|
data: { url: "URL" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servernotices: {
|
||||||
|
name: "サーバーの告知",
|
||||||
|
send: "サーバーの告知を送信",
|
||||||
|
fields: {
|
||||||
|
body: "メッセージ",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "告知を送信",
|
||||||
|
send_success: "サーバーの告知を送信しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "サーバーの告知を指定したユーザーに送信。「サーバーの告知」機能がサーバーで有効になっている必要があります。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user_media_statistics: {
|
||||||
|
name: "ユーザーのメディア",
|
||||||
|
fields: {
|
||||||
|
media_count: "メディア数",
|
||||||
|
media_length: "メディアの大きさ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
forward_extremities: {
|
||||||
|
name: "転送末端",
|
||||||
|
fields: {
|
||||||
|
id: "イベントのID",
|
||||||
|
received_ts: "タイムスタンプ",
|
||||||
|
depth: "深さ",
|
||||||
|
state_group: "ステートのグループ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_state: {
|
||||||
|
name: "ステートイベント",
|
||||||
|
fields: {
|
||||||
|
type: "種類",
|
||||||
|
content: "内容",
|
||||||
|
origin_server_ts: "送信日時",
|
||||||
|
sender: "送信元",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_media: {
|
||||||
|
name: "メディア",
|
||||||
|
fields: {
|
||||||
|
media_id: "メディアのID",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
info: "ルームにアップロードされたメディアファイルの一覧です。外部のレポジトリーにアップロードされたメディアファイルは削除できません。",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
error: "%{errcode} (%{errstatus}) %{error}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_directory: {
|
||||||
|
name: "ルームのディレクトリー",
|
||||||
|
fields: {
|
||||||
|
world_readable: "ゲストユーザーは参加せず閲覧可",
|
||||||
|
guest_can_join: "ゲストユーザーが参加可能",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
title: "ルームをディレクトリーから削除 |||| %{smart_count}個のルームをディレクトリーから削除",
|
||||||
|
content:
|
||||||
|
"このルームをディレクトリーから削除してよろしいですか? |||| %{smart_count}個のルームをディレクトリーから削除してよろしいですか?",
|
||||||
|
erase: "ルームをディレクトリーから削除",
|
||||||
|
create: "ルームをディレクトリーで公開",
|
||||||
|
send_success: "ルームを公開しました。",
|
||||||
|
send_failure: "エラーが発生しました。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destinations: {
|
||||||
|
name: "フェデレーション",
|
||||||
|
fields: {
|
||||||
|
destination: "目的地",
|
||||||
|
failure_ts: "失敗した時点のタイムスタンプ",
|
||||||
|
retry_last_ts: "最後に試行した時点のタイムスタンプ",
|
||||||
|
retry_interval: "再試行までの間隔",
|
||||||
|
last_successful_stream_ordering: "最後に成功したストリーム",
|
||||||
|
stream_ordering: "ストリーム",
|
||||||
|
},
|
||||||
|
action: { reconnect: "再接続" },
|
||||||
|
},
|
||||||
|
registration_tokens: {
|
||||||
|
name: "登録トークン",
|
||||||
|
fields: {
|
||||||
|
token: "トークン",
|
||||||
|
valid: "有効なトークン",
|
||||||
|
uses_allowed: "使用が許可",
|
||||||
|
pending: "保留中",
|
||||||
|
completed: "完了",
|
||||||
|
expiry_time: "期限切れとなる日時",
|
||||||
|
length: "長さ",
|
||||||
|
},
|
||||||
|
helper: { length: "トークンが与えられていない場合のトークンの長さ。" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheduled_commands: {
|
||||||
|
action: {
|
||||||
|
create_success: "スケジュール済のコマンドを作成しました",
|
||||||
|
update_success: "スケジュール済のコマンドを更新しました",
|
||||||
|
update_failure: "エラーが発生しました",
|
||||||
|
delete_success: "スケジュール済のコマンドを削除しました",
|
||||||
|
delete_failure: "エラーが発生しました",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recurring_commands: {
|
||||||
|
action: {
|
||||||
|
create_success: "繰り返しを行うコマンドを作成しました",
|
||||||
|
update_success: "繰り返しを行うコマンドを更新しました",
|
||||||
|
update_failure: "エラーが発生しました",
|
||||||
|
delete_success: "繰り返しを行うコマンドを削除しました",
|
||||||
|
delete_failure: "エラーが発生しました",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default ja;
|
@ -208,11 +208,14 @@ const ru: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "Смена пароля завершит все сессии пользователя.",
|
password: "Смена пароля завершит все сессии пользователя.",
|
||||||
|
password_required_for_reactivation: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
|
||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
suspend:
|
suspend:
|
||||||
"Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
|
"Приостановка учётной записи означает, что пользователь не сможет войти в свою учётную запись, пока она не будет снова активирована.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
|
admin: "Администратор сервера имеет полный контроль над сервером и его пользователями.",
|
||||||
|
lock: "Предотвращает использование пользователем сервера. Это неразрушающее действие, которое может быть отменено.",
|
||||||
erase_text:
|
erase_text:
|
||||||
"Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
"Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
|
@ -193,10 +193,13 @@ const zh: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
password: "更改密码会使用户注销所有会话。",
|
password: "更改密码会使用户注销所有会话。",
|
||||||
|
password_required_for_reactivation: "您必须提供一串密码来激活账户。",
|
||||||
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
create_password: "使用下面的按钮生成一个强大和安全的密码。",
|
||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
suspend: "您必须提供一串密码来暂停账户。",
|
suspend: "您必须提供一串密码来暂停账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
|
admin: "服务器管理员对服务器和其用户有完全的控制权。",
|
||||||
|
lock: "阻止用户使用服务器。这是一个非破坏性的操作,可以被撤销。",
|
||||||
erase_text:
|
erase_text:
|
||||||
"这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
"这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { act, render, screen } from "@testing-library/react";
|
||||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||||
import { AdminContext } from "react-admin";
|
import { AdminContext } from "react-admin";
|
||||||
|
|
||||||
@ -7,7 +7,6 @@ import { AppContext } from "../Context";
|
|||||||
import englishMessages from "../i18n/en";
|
import englishMessages from "../i18n/en";
|
||||||
|
|
||||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||||
import { act } from "@testing-library/react";
|
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders with no restriction to homeserver", async () => {
|
it("renders with no restriction to homeserver", async () => {
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
SelectInput,
|
||||||
useLocales,
|
useLocales,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
@ -51,16 +52,31 @@ const LoginPage = () => {
|
|||||||
restrictBaseUrl.length > 0 &&
|
restrictBaseUrl.length > 0 &&
|
||||||
restrictBaseUrl[0] !== "" &&
|
restrictBaseUrl[0] !== "" &&
|
||||||
restrictBaseUrl[0] !== null;
|
restrictBaseUrl[0] !== null;
|
||||||
|
const baseUrlChoices = allowMultipleBaseUrls ? restrictBaseUrl.map(url => ({ id: url, name: url })) : [];
|
||||||
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||||
|
const localStorageBaseUrl = localStorage.getItem("base_url");
|
||||||
|
let base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorageBaseUrl;
|
||||||
|
if (allowMultipleBaseUrls && localStorageBaseUrl && !restrictBaseUrl.includes(localStorageBaseUrl)) {
|
||||||
|
// don't set base_url if it is not in the restrictBaseUrl array
|
||||||
|
base_url = null;
|
||||||
|
}
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
const [locale, setLocale] = useLocaleState();
|
const [locale, setLocale] = useLocaleState();
|
||||||
const locales = useLocales();
|
const locales = useLocales();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = new URLSearchParams(window.location.search).get("loginToken");
|
const loginToken = new URLSearchParams(window.location.search).get("loginToken");
|
||||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||||
|
const [serverVersion, setServerVersion] = useState("");
|
||||||
|
const [matrixVersions, setMatrixVersions] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (base_url) {
|
||||||
|
checkServerInfo(base_url);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
@ -133,54 +149,75 @@ const LoginPage = () => {
|
|||||||
window.location.href = ssoFullUrl;
|
window.location.href = ssoFullUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkServerInfo = async (url: string) => {
|
||||||
|
if (!isValidBaseUrl(url)) {
|
||||||
|
setServerVersion("");
|
||||||
|
setMatrixVersions("");
|
||||||
|
setSupportPassAuth(false);
|
||||||
|
setSSOBaseUrl("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serverVersion = await getServerVersion(url);
|
||||||
|
setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`);
|
||||||
|
} catch {
|
||||||
|
setServerVersion("");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const features = await getSupportedFeatures(url);
|
||||||
|
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`);
|
||||||
|
} catch {
|
||||||
|
setMatrixVersions("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SSO Url
|
||||||
|
try {
|
||||||
|
const loginFlows = await getSupportedLoginFlows(url);
|
||||||
|
const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
||||||
|
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
||||||
|
setSupportPassAuth(supportPass);
|
||||||
|
setSSOBaseUrl(supportSSO ? url : "");
|
||||||
|
} catch {
|
||||||
|
setSupportPassAuth(false);
|
||||||
|
setSSOBaseUrl("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const UserData = ({ formData }) => {
|
const UserData = ({ formData }) => {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const [serverVersion, setServerVersion] = useState("");
|
|
||||||
const [matrixVersions, setMatrixVersions] = useState("");
|
|
||||||
|
|
||||||
const handleUsernameChange = () => {
|
const handleUsernameChange = async () => {
|
||||||
if (formData.base_url || allowSingleBaseUrl) {
|
if (formData.base_url || allowSingleBaseUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// check if username is a full qualified userId then set base_url accordingly
|
// check if username is a full qualified userId then set base_url accordingly
|
||||||
const domain = splitMxid(formData.username)?.domain;
|
const domain = splitMxid(formData.username)?.domain;
|
||||||
if (domain) {
|
if (domain) {
|
||||||
getWellKnownUrl(domain).then(url => {
|
const url = await getWellKnownUrl(domain);
|
||||||
if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url)))
|
if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url))) {
|
||||||
form.setValue("base_url", url);
|
form.setValue("base_url", url, {
|
||||||
});
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
checkServerInfo(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleBaseUrlBlurOrChange = event => {
|
||||||
if (!formData.base_url) {
|
// Get the value either from the event (onChange) or from formData (onBlur)
|
||||||
form.setValue("base_url", "");
|
const value = event?.target?.value || formData.base_url;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
|
||||||
form.setValue("base_url", restrictBaseUrl[0]);
|
|
||||||
}
|
|
||||||
if (!isValidBaseUrl(formData.base_url)) return;
|
|
||||||
|
|
||||||
getServerVersion(formData.base_url)
|
// Trigger validation only when user finishes typing/selecting
|
||||||
.then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
|
form.trigger("base_url");
|
||||||
.catch(() => setServerVersion(""));
|
checkServerInfo(value);
|
||||||
|
};
|
||||||
getSupportedFeatures(formData.base_url)
|
|
||||||
.then(features =>
|
|
||||||
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`)
|
|
||||||
)
|
|
||||||
.catch(() => setMatrixVersions(""));
|
|
||||||
|
|
||||||
// Set SSO Url
|
|
||||||
getSupportedLoginFlows(formData.base_url)
|
|
||||||
.then(loginFlows => {
|
|
||||||
const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
|
||||||
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
|
||||||
setSupportPassAuth(supportPass);
|
|
||||||
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
|
||||||
})
|
|
||||||
.catch(() => setSSOBaseUrl(""));
|
|
||||||
}, [formData.base_url, form]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@ -189,6 +226,7 @@ const LoginPage = () => {
|
|||||||
const password = params.get("password");
|
const password = params.get("password");
|
||||||
const accessToken = params.get("accessToken");
|
const accessToken = params.get("accessToken");
|
||||||
let serverURL = params.get("server");
|
let serverURL = params.get("server");
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
form.setValue("username", username);
|
form.setValue("username", username);
|
||||||
}
|
}
|
||||||
@ -202,12 +240,19 @@ const LoginPage = () => {
|
|||||||
form.setValue("accessToken", accessToken);
|
form.setValue("accessToken", accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverURL) {
|
if (serverURL) {
|
||||||
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
||||||
if (!isFullUrl) {
|
if (!isFullUrl) {
|
||||||
serverURL = `https://${serverURL}`;
|
serverURL = `https://${serverURL}`;
|
||||||
}
|
}
|
||||||
form.setValue("base_url", serverURL);
|
|
||||||
|
form.setValue("base_url", serverURL, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
checkServerInfo(serverURL);
|
||||||
}
|
}
|
||||||
}, [window.location.search]);
|
}, [window.location.search]);
|
||||||
|
|
||||||
@ -227,7 +272,6 @@ const LoginPage = () => {
|
|||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus
|
|
||||||
source="username"
|
source="username"
|
||||||
label="ra.auth.username"
|
label="ra.auth.username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
@ -261,23 +305,30 @@ const LoginPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
{allowMultipleBaseUrls && (
|
||||||
source="base_url"
|
<SelectInput
|
||||||
label="synapseadmin.auth.base_url"
|
source="base_url"
|
||||||
select={allowMultipleBaseUrls}
|
label="synapseadmin.auth.base_url"
|
||||||
autoComplete="url"
|
select={allowMultipleBaseUrls}
|
||||||
{...(loading ? { disabled: true } : {})}
|
autoComplete="url"
|
||||||
readOnly={allowSingleBaseUrl}
|
{...(loading ? { disabled: true } : {})}
|
||||||
resettable={allowAnyBaseUrl}
|
onChange={handleBaseUrlBlurOrChange}
|
||||||
validate={[required(), validateBaseUrl]}
|
validate={[required(), validateBaseUrl]}
|
||||||
>
|
choices={baseUrlChoices}
|
||||||
{allowMultipleBaseUrls &&
|
/>
|
||||||
restrictBaseUrl.map(url => (
|
)}
|
||||||
<MenuItem key={url} value={url}>
|
{!allowMultipleBaseUrls && (
|
||||||
{url}
|
<TextInput
|
||||||
</MenuItem>
|
source="base_url"
|
||||||
))}
|
label="synapseadmin.auth.base_url"
|
||||||
</TextInput>
|
autoComplete="url"
|
||||||
|
{...(loading ? { disabled: true } : {})}
|
||||||
|
readOnly={allowSingleBaseUrl}
|
||||||
|
resettable={allowAnyBaseUrl}
|
||||||
|
validate={[required(), validateBaseUrl]}
|
||||||
|
onBlur={handleBaseUrlBlurOrChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography className="serverVersion">{serverVersion}</Typography>
|
<Typography className="serverVersion">{serverVersion}</Typography>
|
||||||
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
||||||
@ -286,7 +337,7 @@ const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onBlur">
|
||||||
<LoginFormBox>
|
<LoginFormBox>
|
||||||
<Card className="card">
|
<Card className="card">
|
||||||
<Box className="avatar">
|
<Box className="avatar">
|
||||||
|
@ -114,7 +114,6 @@ const destinationFieldRender = (record: RaRecord) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = (props: ListProps) => {
|
||||||
const record = useRecordContext(props);
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
BooleanInput,
|
BooleanInput,
|
||||||
Create,
|
Create,
|
||||||
CreateProps,
|
CreateProps,
|
||||||
Datagrid,
|
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
DateField,
|
DateField,
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
|
@ -2,7 +2,6 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
|||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import ReportIcon from "@mui/icons-material/Warning";
|
import ReportIcon from "@mui/icons-material/Warning";
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
DateField,
|
DateField,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
|
@ -119,13 +119,9 @@ export const MakeAdminBtn = () => {
|
|||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
try {
|
const result = await dataProvider.makeRoomAdmin(record.room_id, userIdValue);
|
||||||
const result = await dataProvider.makeRoomAdmin(record.room_id, userIdValue);
|
if (!result.success) {
|
||||||
if (!result.success) {
|
throw new Error(result.error);
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -203,7 +199,6 @@ export const MakeAdminBtn = () => {
|
|||||||
|
|
||||||
export const RoomShow = (props: ShowProps) => {
|
export const RoomShow = (props: ShowProps) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const record = useRecordContext();
|
|
||||||
return (
|
return (
|
||||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||||
<TabbedShowLayout>
|
<TabbedShowLayout>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
|
||||||
DatagridConfigurable,
|
DatagridConfigurable,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
List,
|
List,
|
||||||
|
@ -115,7 +115,8 @@ const userFilters = [
|
|||||||
<BooleanInput source="guests" alwaysOn />,
|
<BooleanInput source="guests" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
|
// waiting for https://github.com/element-hq/synapse/issues/18016
|
||||||
|
// <BooleanInput label="resources.users.fields.show_suspended" source="suspended" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserPreventSelfDelete: React.FC<{
|
const UserPreventSelfDelete: React.FC<{
|
||||||
@ -208,7 +209,6 @@ export const UserList = (props: ListProps) => (
|
|||||||
<BooleanField source="admin" label="resources.users.fields.admin" />
|
<BooleanField source="admin" label="resources.users.fields.admin" />
|
||||||
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
|
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
|
||||||
<BooleanField source="locked" label="resources.users.fields.locked" />
|
<BooleanField source="locked" label="resources.users.fields.locked" />
|
||||||
<BooleanField source="suspended" label="resources.users.fields.suspended" />
|
|
||||||
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
|
||||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
</DatagridConfigurable>
|
</DatagridConfigurable>
|
||||||
@ -219,13 +219,12 @@ export const UserList = (props: ListProps) => (
|
|||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||||
// storage.getItem("home_server").length is not valid here
|
// storage.getItem("home_server").length is not valid here
|
||||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-\+/]+$/, "synapseadmin.users.invalid_user_id")];
|
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-+/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||||
|
|
||||||
const validateAddress = [required(), maxLength(255)];
|
const validateAddress = [required(), maxLength(255)];
|
||||||
|
|
||||||
const UserEditActions = () => {
|
const UserEditActions = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
let asManagedUserIsSelected = false;
|
let asManagedUserIsSelected = false;
|
||||||
@ -262,6 +261,7 @@ export const UserCreate = (props: CreateProps) => {
|
|||||||
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
|
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
|
||||||
<Typography component="span"></Typography>
|
<Typography component="span"></Typography>
|
||||||
);
|
);
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
const [create] = useCreate();
|
const [create] = useCreate();
|
||||||
|
|
||||||
@ -284,6 +284,7 @@ export const UserCreate = (props: CreateProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const postSave = (data: Record<string, any>) => {
|
const postSave = (data: Record<string, any>) => {
|
||||||
setFormData(data);
|
setFormData(data);
|
||||||
if (!userIsAvailable) {
|
if (!userIsAvailable) {
|
||||||
@ -426,7 +427,7 @@ const UserBooleanInput = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BooleanInput {...props} disabled={ownUserIsSelected || asManagedUserIsSelected} />
|
<BooleanInput disabled={ownUserIsSelected || asManagedUserIsSelected} {...props} />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -434,6 +435,7 @@ const UserBooleanInput = props => {
|
|||||||
const UserPasswordInput = props => {
|
const UserPasswordInput = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
let asManagedUserIsSelected = false;
|
let asManagedUserIsSelected = false;
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
// Get form context to update field value
|
// Get form context to update field value
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
@ -446,17 +448,34 @@ const UserPasswordInput = props => {
|
|||||||
form.setValue("password", password, { shouldDirty: true });
|
form.setValue("password", password, { shouldDirty: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get the current deactivated state and the original value
|
||||||
|
const deactivated = form.watch("deactivated");
|
||||||
|
const deactivatedFromRecord = record?.deactivated;
|
||||||
|
|
||||||
|
// Custom validation for reactivation case
|
||||||
|
const validatePasswordOnReactivation = value => {
|
||||||
|
if (deactivatedFromRecord === true && deactivated === false && !value) {
|
||||||
|
return translate("resources.users.helper.password_required_for_reactivation");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
let passwordHelperText = "resources.users.helper.create_password";
|
||||||
|
|
||||||
|
if (asManagedUserIsSelected) {
|
||||||
|
passwordHelperText = "resources.users.helper.modify_managed_user_error";
|
||||||
|
} else if (deactivatedFromRecord === true && deactivated === false) {
|
||||||
|
passwordHelperText = "resources.users.helper.password_required_for_reactivation";
|
||||||
|
} else if (record) {
|
||||||
|
passwordHelperText = "resources.users.helper.password";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
{...props}
|
{...props}
|
||||||
helperText={
|
validate={validatePasswordOnReactivation}
|
||||||
asManagedUserIsSelected
|
helperText={passwordHelperText}
|
||||||
? "resources.users.helper.modify_managed_user_error"
|
|
||||||
: record
|
|
||||||
? "resources.users.helper.password"
|
|
||||||
: "resources.users.helper.create_password"
|
|
||||||
}
|
|
||||||
disabled={asManagedUserIsSelected}
|
disabled={asManagedUserIsSelected}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -470,8 +489,28 @@ const UserPasswordInput = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ErasedBooleanInput = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
const form = useFormContext();
|
||||||
|
const deactivated = form.watch("deactivated");
|
||||||
|
const erased = form.watch("erased");
|
||||||
|
|
||||||
|
const erasedFromRecord = record?.erased;
|
||||||
|
const deactivatedFromRecord = record?.deactivated;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the user was erased and deactivated, by unchecking Erased, we want to also uncheck Deactivated
|
||||||
|
if (erasedFromRecord === true && erased === false) {
|
||||||
|
form.setValue("deactivated", false);
|
||||||
|
}
|
||||||
|
}, [deactivatedFromRecord, erased, erasedFromRecord]);
|
||||||
|
|
||||||
|
return <UserBooleanInput disabled={!deactivated} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const UserEdit = (props: EditProps) => {
|
export const UserEdit = (props: EditProps) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit
|
<Edit
|
||||||
@ -515,12 +554,24 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
helperText="resources.users.helper.password"
|
helperText="resources.users.helper.password"
|
||||||
/>
|
/>
|
||||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" helperText="resources.users.helper.admin" />
|
||||||
<UserBooleanInput source="locked" />
|
|
||||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
|
||||||
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
<UserBooleanInput source="suspended" helperText="resources.users.helper.suspend" />
|
||||||
<BooleanInput source="erased" disabled />
|
<UserBooleanInput
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
sx={{ color: theme.palette.warning.main }}
|
||||||
|
source="locked"
|
||||||
|
helperText="resources.users.helper.lock"
|
||||||
|
/>
|
||||||
|
<UserBooleanInput
|
||||||
|
sx={{ color: theme.palette.error.main }}
|
||||||
|
source="deactivated"
|
||||||
|
helperText="resources.users.helper.deactivate"
|
||||||
|
/>
|
||||||
|
<ErasedBooleanInput
|
||||||
|
sx={{ color: theme.palette.error.main, marginLeft: "25px" }}
|
||||||
|
source="erased"
|
||||||
|
helperText="resources.users.helper.erase"
|
||||||
|
/>
|
||||||
|
<DateField sx={{ marginTop: "20px" }} source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="consent_version" />
|
<TextField source="consent_version" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import { GetConfig } from "../utils/config";
|
|||||||
import { MatrixError, displayError } from "../utils/error";
|
import { MatrixError, displayError } from "../utils/error";
|
||||||
import { returnMXID } from "../utils/mxid";
|
import { returnMXID } from "../utils/mxid";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const CACHED_MANY_REF: Record<string, any> = {};
|
const CACHED_MANY_REF: Record<string, any> = {};
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
@ -33,7 +34,7 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
|||||||
try {
|
try {
|
||||||
const response = await fetchUtils.fetchJson(url, options);
|
const response = await fetchUtils.fetchJson(url, options);
|
||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
const error = err as HttpError;
|
const error = err as HttpError;
|
||||||
const errorStatus = error.status;
|
const errorStatus = error.status;
|
||||||
const errorBody = error.body as MatrixError;
|
const errorBody = error.body as MatrixError;
|
||||||
@ -45,16 +46,11 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const filterUndefined = (obj: Record<string, any>) => {
|
const filterUndefined = (obj: Record<string, any>) => {
|
||||||
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
|
return Object.fromEntries(Object.entries(obj).filter(([_key, value]) => value !== undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Action {
|
|
||||||
endpoint: string;
|
|
||||||
method?: string;
|
|
||||||
body?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -338,6 +334,19 @@ export interface RecurringCommand {
|
|||||||
time: string;
|
time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Payment {
|
||||||
|
amount: number;
|
||||||
|
email: string;
|
||||||
|
is_subscription: boolean;
|
||||||
|
paid_at: string;
|
||||||
|
transaction_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentsResponse {
|
||||||
|
payments: Payment[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
@ -348,6 +357,11 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
getAccountData: (id: Identifier) => Promise<AccountDataModel>;
|
getAccountData: (id: Identifier) => Promise<AccountDataModel>;
|
||||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
|
suspendUser: (
|
||||||
|
id: Identifier,
|
||||||
|
suspendValue: boolean
|
||||||
|
) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
|
eraseUser: (id: Identifier) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||||
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||||
@ -361,6 +375,8 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
createRecurringCommand: (etkeAdminUrl: string, command: Partial<RecurringCommand>) => Promise<RecurringCommand>;
|
createRecurringCommand: (etkeAdminUrl: string, command: Partial<RecurringCommand>) => Promise<RecurringCommand>;
|
||||||
updateRecurringCommand: (etkeAdminUrl: string, command: RecurringCommand) => Promise<RecurringCommand>;
|
updateRecurringCommand: (etkeAdminUrl: string, command: RecurringCommand) => Promise<RecurringCommand>;
|
||||||
deleteRecurringCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
|
deleteRecurringCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
|
||||||
|
getPayments: (etkeAdminUrl: string) => Promise<PaymentsResponse>;
|
||||||
|
getInvoice: (etkeAdminUrl: string, transactionId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@ -782,6 +798,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
|
|
||||||
const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
body: JSON.stringify(params.data, filterNullValues),
|
||||||
@ -984,7 +1001,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
},
|
},
|
||||||
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
||||||
const filtered = Object.entries(rateLimits)
|
const filtered = Object.entries(rateLimits)
|
||||||
.filter(([key, value]) => value !== null && value !== undefined)
|
.filter(([_key, value]) => value !== null && value !== undefined)
|
||||||
.reduce((obj, [key, value]) => {
|
.reduce((obj, [key, value]) => {
|
||||||
obj[key] = value;
|
obj[key] = value;
|
||||||
return obj;
|
return obj;
|
||||||
@ -1017,7 +1034,39 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
const endpoint_url = `${base_url}/_synapse/admin/v1/rooms/${encodeURIComponent(room_id)}/make_room_admin`;
|
const endpoint_url = `${base_url}/_synapse/admin/v1/rooms/${encodeURIComponent(room_id)}/make_room_admin`;
|
||||||
try {
|
try {
|
||||||
const { json } = await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ user_id }) });
|
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ user_id }) });
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
return { success: false, error: error.body.error, errcode: error.body.errcode };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
suspendUser: async (id: Identifier, suspendValue: boolean) => {
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/suspend/${encodeURIComponent(returnMXID(id))}`;
|
||||||
|
try {
|
||||||
|
await jsonClient(endpoint_url, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ suspend: suspendValue }),
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
return { success: false, error: error.body.error, errcode: error.body.errcode };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eraseUser: async (id: Identifier) => {
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/deactivate/${encodeURIComponent(returnMXID(id))}`;
|
||||||
|
try {
|
||||||
|
await jsonClient(endpoint_url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ erase: true }),
|
||||||
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
@ -1173,7 +1222,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
return {};
|
return {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching server commands, error");
|
console.error("Error fetching server commands:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@ -1233,7 +1282,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching scheduled commands, error");
|
console.error("Error fetching scheduled commands:", error);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
@ -1258,7 +1307,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching recurring commands, error");
|
console.error("Error fetching recurring commands:", error);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
@ -1418,6 +1467,92 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getPayments: async (etkeAdminUrl: string) => {
|
||||||
|
const response = await fetch(`${etkeAdminUrl}/payments`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch payments: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
return json as PaymentsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 204) {
|
||||||
|
return { payments: [], total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${response.status} ${response.statusText}`); // Handle unexpected status codes
|
||||||
|
},
|
||||||
|
getInvoice: async (etkeAdminUrl: string, transactionId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${etkeAdminUrl}/payments/${transactionId}/invoice`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `Error fetching invoice: ${response.status} ${response.statusText}`;
|
||||||
|
|
||||||
|
// Handle specific error codes
|
||||||
|
switch (response.status) {
|
||||||
|
case 404:
|
||||||
|
errorMessage = "Invoice not found for this transaction";
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errorMessage = "Server error while generating invoice. Please try again later";
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
errorMessage = "Unauthorized access. Please check your permissions";
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
errorMessage = "Access forbidden. You don't have permission to download this invoice";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = `Failed to fetch invoice (${response.status}): ${response.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file as a blob
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Create a download link
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// Try to get filename from response headers
|
||||||
|
const contentDisposition = response.headers.get("Content-Disposition");
|
||||||
|
let filename = `invoice_${transactionId}.pdf`;
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
if (filenameMatch) {
|
||||||
|
filename = filenameMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading invoice:", error);
|
||||||
|
throw error; // Re-throw to let the UI handle the error
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
@ -1427,28 +1562,43 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
|||||||
const avatarFile = params.data.avatar_file?.rawFile;
|
const avatarFile = params.data.avatar_file?.rawFile;
|
||||||
const avatarErase = params.data.avatar_erase;
|
const avatarErase = params.data.avatar_erase;
|
||||||
const rates = params.data.rates;
|
const rates = params.data.rates;
|
||||||
|
const suspended = params.data.suspended;
|
||||||
|
const previousSuspended = params.previousData?.suspended;
|
||||||
|
const deactivated = params.data.deactivated;
|
||||||
|
const erased = params.data.erased;
|
||||||
|
|
||||||
if (rates) {
|
if (rates) {
|
||||||
await dataProvider.setRateLimits(params.id, rates);
|
await dataProvider.setRateLimits(params.id, rates);
|
||||||
delete params.data.rates;
|
delete params.data.rates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (suspended !== undefined && suspended !== previousSuspended) {
|
||||||
|
await (dataProvider as SynapseDataProvider).suspendUser(params.id, suspended);
|
||||||
|
delete params.data.suspended;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deactivated !== undefined && erased !== undefined) {
|
||||||
|
await (dataProvider as SynapseDataProvider).eraseUser(params.id);
|
||||||
|
delete params.data.deactivated;
|
||||||
|
delete params.data.erased;
|
||||||
|
}
|
||||||
|
|
||||||
if (avatarErase) {
|
if (avatarErase) {
|
||||||
params.data.avatar_url = "";
|
params.data.avatar_url = "";
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarFile instanceof File) {
|
if (avatarFile instanceof File) {
|
||||||
const reponse = await dataProvider.uploadMedia({
|
const response = await dataProvider.uploadMedia({
|
||||||
file: avatarFile,
|
file: avatarFile,
|
||||||
filename: params.data.avatar_file.title,
|
filename: params.data.avatar_file.title,
|
||||||
content_type: params.data.avatar_file.rawFile.type,
|
content_type: params.data.avatar_file.rawFile.type,
|
||||||
});
|
});
|
||||||
params.data.avatar_url = reponse.content_uri;
|
params.data.avatar_url = response.content_uri;
|
||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
beforeDelete: async (params: DeleteParams<any>, dataProvider: DataProvider) => {
|
beforeDelete: async (params: DeleteParams<any>, _dataProvider: DataProvider) => {
|
||||||
if (params.meta?.deleteMedia) {
|
if (params.meta?.deleteMedia) {
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
|
||||||
@ -1463,7 +1613,7 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
|||||||
|
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
beforeDeleteMany: async (params: DeleteManyParams<any>, dataProvider: DataProvider) => {
|
beforeDeleteMany: async (params: DeleteManyParams<any>, _dataProvider: DataProvider) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
params.ids.map(async id => {
|
params.ids.map(async id => {
|
||||||
if (params.meta?.deleteMedia) {
|
if (params.meta?.deleteMedia) {
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Identifier, fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
import { isMXID } from "../utils/mxid";
|
|
||||||
|
|
||||||
export const splitMxid = mxid => {
|
export const splitMxid = mxid => {
|
||||||
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
||||||
|
@ -67,9 +67,9 @@ export const FetchConfig = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// load config from context
|
// load config from context
|
||||||
// we deliberately processing each key separately to avoid overwriting the whole config, loosing some keys, and messing
|
// we deliberately processing each key separately to avoid overwriting the whole config, losing some keys, and messing
|
||||||
// with typescript types
|
// with typescript types
|
||||||
export const LoadConfig = (context: any) => {
|
export const LoadConfig = (context: object) => {
|
||||||
if (context?.restrictBaseUrl) {
|
if (context?.restrictBaseUrl) {
|
||||||
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
* @returns The decoded string, or the original string if decoding fails.
|
* @returns The decoded string, or the original string if decoding fails.
|
||||||
* @example decodeURIComponent("Hello%20World") // "Hello World"
|
* @example decodeURIComponent("Hello%20World") // "Hello World"
|
||||||
*/
|
*/
|
||||||
const decodeURLComponent = (str: any): any => {
|
const decodeURLComponent = (str: string): string => {
|
||||||
try {
|
try {
|
||||||
return decodeURIComponent(str);
|
return decodeURIComponent(str);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: str
|
|||||||
const re = /^mxc:\/\/([^/]+)\/([\w-]+)$/;
|
const re = /^mxc:\/\/([^/]+)\/([\w-]+)$/;
|
||||||
const ret = re.exec(mxcUrl);
|
const ret = re.exec(mxcUrl);
|
||||||
if (ret == null) {
|
if (ret == null) {
|
||||||
throw new Error("Invalid mxcUrl");
|
return { serverName: "", mediaId: "" };
|
||||||
}
|
}
|
||||||
const serverName = ret[1];
|
const serverName = ret[1];
|
||||||
const mediaId = ret[2];
|
const mediaId = ret[2];
|
||||||
@ -17,7 +17,8 @@ export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType):
|
|||||||
|
|
||||||
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
|
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
|
||||||
if (!serverName || !mediaId) {
|
if (!serverName || !mediaId) {
|
||||||
throw new Error("Invalid mxcUrl");
|
console.error("Invalid mxcUrl", mxcUrl, "serverName:", serverName, "mediaId:", mediaId);
|
||||||
|
return new Response(null, { status: 400, statusText: "Invalid mxcUrl" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = "";
|
let url = "";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user