Compare commits
No commits in common. "main" and "v0.11.1-etke44" have entirely different histories.
main
...
v0.11.1-et
@ -1,33 +0,0 @@
|
||||
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
Normal file
63
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
# 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
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
liberapay: etkecc
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
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
Normal file
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
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
Normal file
14
.github/SECURITY.md
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# 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
Normal file
19
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
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
Normal file
121
.github/workflows/workflow.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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,11 +1,5 @@
|
||||
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
|
||||
|
||||
COPY ./dist /app
|
||||
|
11
Dockerfile.build
Normal file
11
Dockerfile.build
Normal file
@ -0,0 +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
|
||||
ENV SERVER_ROOT=/app
|
||||
COPY --from=builder /src/dist /app
|
@ -1,5 +0,0 @@
|
||||
FROM ghcr.io/static-web-server/static-web-server:2
|
||||
|
||||
ENV SERVER_ROOT=/app
|
||||
|
||||
COPY ./dist /app
|
@ -113,11 +113,9 @@ The following changes are already implemented:
|
||||
* 🗂️ [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)
|
||||
* 🌐 [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)
|
||||
* [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)
|
||||
|
||||
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||
|
||||
@ -128,7 +126,6 @@ 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 Commands panel](https://github.com/etkecc/synapse-admin/pull/365)
|
||||
* 🚀 [Server Actions page](https://github.com/etkecc/synapse-admin/pull/457)
|
||||
* 💳 [Billing page](https://github.com/etkecc/synapse-admin/pull/691)
|
||||
|
||||
### Development
|
||||
|
||||
|
@ -10,7 +10,7 @@ const config: JestConfigWithTsJest = {
|
||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
||||
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
||||
transform: {
|
||||
"^.+\\.(js|jsx|ts|tsx)$": [
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
diagnostics: {
|
||||
@ -27,6 +27,5 @@ const config: JestConfigWithTsJest = {
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: [],
|
||||
};
|
||||
export default config;
|
||||
|
102
package.json
102
package.json
@ -1,76 +1,56 @@
|
||||
{
|
||||
"name": "synapse-admin",
|
||||
"version": "0.11.1",
|
||||
"description": "Feature-packed and visually customizable admin GUI for Matrix Synapse servers.",
|
||||
"keywords": [
|
||||
"matrix",
|
||||
"synapse",
|
||||
"admin",
|
||||
"homeserver",
|
||||
"management",
|
||||
"react",
|
||||
"nodejs",
|
||||
"dashboard",
|
||||
"etkecc",
|
||||
"docker"
|
||||
],
|
||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||
"type": "module",
|
||||
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/etkecc/synapse-admin#readme",
|
||||
"homepage": ".",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"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"
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"@typescript-eslint/parser": "^8.32.0",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"jest": "^30.0.5",
|
||||
"jest-environment-jsdom": "^30.0.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"prettier": "^3.6.2",
|
||||
"react-test-renderer": "^19.1.1",
|
||||
"ts-jest": "^29.4.1",
|
||||
"prettier": "^3.5.3",
|
||||
"react-test-renderer": "^19.1.0",
|
||||
"ts-jest": "^29.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-version-mark": "^0.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bicstone/ra-language-japanese": "^5.10.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/utils": "^7.1.0",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"history": "^5.3.0",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
@ -79,17 +59,17 @@
|
||||
"ra-i18n-polyglot": "^5.4.4",
|
||||
"ra-language-english": "^5.4.4",
|
||||
"ra-language-farsi": "^5.1.0",
|
||||
"ra-language-french": "^5.10.1",
|
||||
"ra-language-french": "^5.8.3",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^5.4.4",
|
||||
"react": "^19.1.1",
|
||||
"react-admin": "^5.10.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-is": "^19.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-admin": "^5.8.3",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-is": "^19.1.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^7.8.0",
|
||||
"ts-jest-mock-import-meta": "^1.3.1"
|
||||
"react-router-dom": "^7.6.2",
|
||||
"ts-jest-mock-import-meta": "^1.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite serve",
|
||||
@ -122,18 +102,6 @@
|
||||
"root": true,
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"args": "all",
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "all",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
11
src/App.tsx
11
src/App.tsx
@ -5,7 +5,6 @@ import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin
|
||||
import { Route } from "react-router-dom";
|
||||
|
||||
import AdminLayout from "./components/AdminLayout";
|
||||
import BillingPage from "./components/etke.cc/BillingPage";
|
||||
import ServerActionsPage from "./components/etke.cc/ServerActionsPage";
|
||||
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||
@ -17,7 +16,6 @@ import germanMessages from "./i18n/de";
|
||||
import englishMessages from "./i18n/en";
|
||||
import frenchMessages from "./i18n/fr";
|
||||
import italianMessages from "./i18n/it";
|
||||
import japaneseMessages from "./i18n/ja";
|
||||
import russianMessages from "./i18n/ru";
|
||||
import chineseMessages from "./i18n/zh";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
@ -37,7 +35,6 @@ const messages = {
|
||||
en: englishMessages,
|
||||
fr: frenchMessages,
|
||||
it: italianMessages,
|
||||
ja: japaneseMessages,
|
||||
ru: russianMessages,
|
||||
zh: chineseMessages,
|
||||
};
|
||||
@ -49,10 +46,9 @@ const i18nProvider = polyglotI18nProvider(
|
||||
{ locale: "de", name: "Deutsch" },
|
||||
{ locale: "fr", name: "Français" },
|
||||
{ locale: "it", name: "Italiano" },
|
||||
{ locale: "ja", name: "Japanese (日本語)" },
|
||||
{ locale: "fa", name: "Persian (فارسی)" },
|
||||
{ locale: "ru", name: "Russian (Русский)" },
|
||||
{ locale: "zh", name: "Chinese (简体中文)" },
|
||||
{ locale: "fa", name: "Persian(فارسی)" },
|
||||
{ locale: "ru", name: "Russian(Русский)" },
|
||||
{ locale: "zh", name: "简体中文" },
|
||||
]
|
||||
);
|
||||
|
||||
@ -80,7 +76,6 @@ export const App = () => (
|
||||
<Route path="/server_actions/recurring/:id" element={<RecurringCommandEdit />} />
|
||||
<Route path="/server_actions/recurring/create" element={<RecurringCommandEdit />} />
|
||||
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||
<Route path="/billing" element={<BillingPage />} />
|
||||
</CustomRoutes>
|
||||
<Resource {...users} />
|
||||
<Resource {...rooms} />
|
||||
|
@ -1,5 +1,4 @@
|
||||
import ManageHistoryIcon from "@mui/icons-material/ManageHistory";
|
||||
import PaymentIcon from "@mui/icons-material/Payment";
|
||||
import { useEffect, useState, Suspense } from "react";
|
||||
import {
|
||||
CheckForApplicationUpdate,
|
||||
@ -13,7 +12,6 @@ import {
|
||||
useLogout,
|
||||
UserMenu,
|
||||
useStore,
|
||||
useLocaleState,
|
||||
} from "react-admin";
|
||||
|
||||
import Footer from "./Footer";
|
||||
@ -85,11 +83,11 @@ const AdminMenu = props => {
|
||||
setEtkeRoutesEnabled(true);
|
||||
}
|
||||
}, []);
|
||||
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
command: "",
|
||||
locked_at: "",
|
||||
});
|
||||
const [serverStatus, _setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||
success: false,
|
||||
ok: false,
|
||||
host: "",
|
||||
@ -122,12 +120,10 @@ const AdminMenu = props => {
|
||||
primaryText="Server Actions"
|
||||
/>
|
||||
)}
|
||||
{etkeRoutesEnabled && <Menu.Item key="billing" to="/billing" leftIcon={<PaymentIcon />} primaryText="Billing" />}
|
||||
<Menu.ResourceItems />
|
||||
{menu &&
|
||||
menu.map((item, index) => {
|
||||
const { url, icon, label } = item;
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||
|
||||
return (
|
||||
@ -147,12 +143,6 @@ const AdminMenu = props => {
|
||||
};
|
||||
|
||||
export const AdminLayout = ({ children }) => {
|
||||
// Set the document language based on the selected locale
|
||||
const [locale, _setLocale] = useLocaleState();
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale;
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout
|
||||
|
@ -7,8 +7,10 @@ import {
|
||||
SimpleForm,
|
||||
BooleanInput,
|
||||
useTranslate,
|
||||
RaRecord,
|
||||
useNotify,
|
||||
useRedirect,
|
||||
useDelete,
|
||||
NotificationType,
|
||||
useDeleteMany,
|
||||
Identifier,
|
||||
@ -49,7 +51,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = props => {
|
||||
unselectAll();
|
||||
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 }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -7,8 +7,10 @@ import {
|
||||
SimpleForm,
|
||||
BooleanInput,
|
||||
useTranslate,
|
||||
RaRecord,
|
||||
useNotify,
|
||||
useRedirect,
|
||||
useDelete,
|
||||
NotificationType,
|
||||
useDeleteMany,
|
||||
Identifier,
|
||||
@ -55,7 +57,7 @@ const DeleteUserButton: React.FC<DeleteUserButtonProps> = props => {
|
||||
unselectAll();
|
||||
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 }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ export const ExperimentalFeaturesList = () => {
|
||||
const updateFeature = async (feature_name: string, feature_value: boolean) => {
|
||||
const updatedFeatures = { ...features, [feature_name]: feature_value } as ExperimentalFeaturesModel;
|
||||
setFeatures(updatedFeatures);
|
||||
await dataProvider.updateFeatures(record.id, updatedFeatures);
|
||||
const response = await dataProvider.updateFeatures(record.id, updatedFeatures);
|
||||
notify("ra.notification.updated", {
|
||||
messageArgs: { smart_count: 1 },
|
||||
type: "success",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Avatar, Box, Link } from "@mui/material";
|
||||
import { Avatar, Box, Link, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
@ -29,7 +29,7 @@ const UserAccountData = () => {
|
||||
return (
|
||||
<Typography variant="body2">
|
||||
{translate("ra.navigation.no_results", {
|
||||
name: "Account Data",
|
||||
resource: "Account Data",
|
||||
_: "No results found.",
|
||||
})}
|
||||
</Typography>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { TextField } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
|
||||
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
const RateLimitRow = ({
|
||||
@ -10,8 +10,8 @@ const RateLimitRow = ({
|
||||
updateRateLimit,
|
||||
}: {
|
||||
limit: string;
|
||||
value: object;
|
||||
updateRateLimit: (limit: string, value: integer | null) => void;
|
||||
value: any;
|
||||
updateRateLimit: (limit: string, value: any) => void;
|
||||
}) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
@ -53,6 +53,8 @@ const RateLimitRow = ({
|
||||
};
|
||||
|
||||
const UserRateLimits = () => {
|
||||
const translate = useTranslate();
|
||||
const notify = useNotify();
|
||||
const record = useRecordContext();
|
||||
const form = useFormContext();
|
||||
const dataProvider = useDataProvider();
|
||||
@ -76,7 +78,7 @@ const UserRateLimits = () => {
|
||||
fetchRateLimits();
|
||||
}, []);
|
||||
|
||||
const updateRateLimit = async (limit: string, value: integer | null) => {
|
||||
const updateRateLimit = async (limit: string, value: any) => {
|
||||
const updatedRateLimits = { ...rateLimits, [limit]: value };
|
||||
setRateLimits(updatedRateLimits);
|
||||
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
|
||||
|
@ -1,214 +0,0 @@
|
||||
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";
|
||||
|
||||
const CurrentlyRunningCommand = () => {
|
||||
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
command: "",
|
||||
locked_at: "",
|
||||
});
|
||||
|
@ -65,10 +65,3 @@ On this page you can do the following:
|
||||
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.
|
||||
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 ScheduleIcon from "@mui/icons-material/Schedule";
|
||||
import { Box, Typography, Link } from "@mui/material";
|
||||
import { Box, Typography, Link, Divider } from "@mui/material";
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||
|
@ -23,7 +23,6 @@ import { ServerCommand, ServerProcessResponse } from "../../synapse/dataProvider
|
||||
import { Icons } from "../../utils/icons";
|
||||
|
||||
const renderIcon = (icon: string) => {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||
return IconComponent ? <IconComponent sx={{ verticalAlign: "middle", mr: 1 }} /> : null;
|
||||
};
|
||||
@ -81,7 +80,6 @@ const ServerCommandsPanel = () => {
|
||||
// Update server process status
|
||||
await updateServerProcessStatus(serverCommands[command]);
|
||||
} catch (error) {
|
||||
console.error("Error running command:", error);
|
||||
setCommandIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
@ -33,7 +33,7 @@ const useServerNotifications = () => {
|
||||
notifications: [],
|
||||
success: false,
|
||||
});
|
||||
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
command: "",
|
||||
locked_at: "",
|
||||
});
|
||||
|
@ -59,11 +59,11 @@ const useServerStatus = () => {
|
||||
host: "",
|
||||
results: [],
|
||||
});
|
||||
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
command: "",
|
||||
locked_at: "",
|
||||
});
|
||||
const { command } = serverProcess;
|
||||
const { command, locked_at } = serverProcess;
|
||||
const { etkeccAdmin } = useAppContext();
|
||||
const dataProvider = useDataProvider();
|
||||
const isOkay = serverStatus.ok;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import EngineeringIcon from "@mui/icons-material/Engineering";
|
||||
import { Box, Stack, Typography, Paper, Link, Chip, Divider, ChipProps } from "@mui/material";
|
||||
import { Alert, Box, Stack, Typography, Paper, Link, Chip, Divider, Tooltip, ChipProps } from "@mui/material";
|
||||
import { useStore } from "ra-core";
|
||||
|
||||
import CurrentlyRunningCommand from "./CurrentlyRunningCommand";
|
||||
import { ServerProcessResponse, ServerStatusComponent, ServerStatusResponse } from "../../synapse/dataProvider";
|
||||
import { getTimeSince } from "../../utils/date";
|
||||
|
||||
const StatusChip = ({
|
||||
isOkay,
|
||||
@ -39,17 +40,17 @@ const ServerComponentText = ({ text }: { text: string }) => {
|
||||
};
|
||||
|
||||
const ServerStatusPage = () => {
|
||||
const [serverStatus, _setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||
const [serverStatus, setServerStatus] = useStore<ServerStatusResponse>("serverStatus", {
|
||||
ok: false,
|
||||
success: false,
|
||||
host: "",
|
||||
results: [],
|
||||
});
|
||||
const [serverProcess, _setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
const [serverProcess, setServerProcess] = useStore<ServerProcessResponse>("serverProcess", {
|
||||
command: "",
|
||||
locked_at: "",
|
||||
});
|
||||
const { command } = serverProcess;
|
||||
const { command, locked_at } = serverProcess;
|
||||
const successCheck = serverStatus.success;
|
||||
const isOkay = serverStatus.ok;
|
||||
const host = serverStatus.host;
|
||||
@ -103,7 +104,7 @@ const ServerStatusPage = () => {
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2} direction="row">
|
||||
{Object.keys(groupedResults).map((category, _idx) => (
|
||||
{Object.keys(groupedResults).map((category, idx) => (
|
||||
<Box key={`category_${category}`} sx={{ flex: 1 }}>
|
||||
<Typography variant="h5" mb={1}>
|
||||
{category}
|
||||
|
@ -0,0 +1,21 @@
|
||||
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,7 +22,6 @@ import { RecurringCommand } from "../../../../../synapse/dataProvider";
|
||||
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||
import { useRecurringCommands } from "../../hooks/useRecurringCommands";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||
return Object.entries(commands).map(([key, value]) => ({
|
||||
id: key,
|
||||
@ -112,11 +111,13 @@ const RecurringCommandEdit = () => {
|
||||
delete submissionData.args;
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (isCreating) {
|
||||
await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
|
||||
result = await dataProvider.createRecurringCommand(etkeccAdmin, submissionData);
|
||||
notify("recurring_commands.action.create_success", { type: "success" });
|
||||
} else {
|
||||
await dataProvider.updateRecurringCommand(etkeccAdmin, {
|
||||
result = await dataProvider.updateRecurringCommand(etkeccAdmin, {
|
||||
...submissionData,
|
||||
id: id,
|
||||
});
|
||||
@ -128,7 +129,6 @@ const RecurringCommandEdit = () => {
|
||||
|
||||
navigate("/server_actions");
|
||||
} catch (error) {
|
||||
console.error("Error saving recurring command:", error);
|
||||
notify("recurring_commands.action.update_failure", { type: "error" });
|
||||
}
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ const ListActions = () => {
|
||||
};
|
||||
|
||||
const RecurringCommandsList = () => {
|
||||
const { data, isLoading } = useRecurringCommands();
|
||||
const { data, isLoading, error } = useRecurringCommands();
|
||||
|
||||
const listContext = useList({
|
||||
resource: "recurring",
|
||||
@ -40,7 +40,6 @@ const RecurringCommandsList = () => {
|
||||
<Paper>
|
||||
<Datagrid
|
||||
bulkActionButtons={false}
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||
if (!record) {
|
||||
return "";
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
useDataProvider,
|
||||
Loading,
|
||||
Button,
|
||||
BooleanInput,
|
||||
SelectInput,
|
||||
} from "react-admin";
|
||||
import { useWatch } from "react-hook-form";
|
||||
@ -22,7 +23,6 @@ import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||
import { useServerCommands } from "../../../hooks/useServerCommands";
|
||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const transformCommandsToChoices = (commands: Record<string, any>) => {
|
||||
return Object.entries(commands).map(([key, value]) => ({
|
||||
id: key,
|
||||
@ -50,7 +50,7 @@ const ScheduledCommandEdit = () => {
|
||||
const isCreating = typeof id === "undefined";
|
||||
const [loading, setLoading] = useState(!isCreating);
|
||||
const { data: scheduledCommands, isLoading: isLoadingList } = useScheduledCommands();
|
||||
const { serverCommands } = useServerCommands();
|
||||
const { serverCommands, isLoading: isLoadingServerCommands } = useServerCommands();
|
||||
const pageTitle = isCreating ? "Create Scheduled Command" : "Edit Scheduled Command";
|
||||
|
||||
const commandChoices = transformCommandsToChoices(serverCommands);
|
||||
@ -67,12 +67,15 @@ const ScheduledCommandEdit = () => {
|
||||
|
||||
const handleSubmit = async data => {
|
||||
try {
|
||||
let result;
|
||||
|
||||
data.scheduled_at = new Date(data.scheduled_at).toISOString();
|
||||
|
||||
if (isCreating) {
|
||||
await dataProvider.createScheduledCommand(etkeccAdmin, data);
|
||||
result = await dataProvider.createScheduledCommand(etkeccAdmin, data);
|
||||
notify("scheduled_commands.action.create_success", { type: "success" });
|
||||
} else {
|
||||
await dataProvider.updateScheduledCommand(etkeccAdmin, {
|
||||
result = await dataProvider.updateScheduledCommand(etkeccAdmin, {
|
||||
...data,
|
||||
id: id,
|
||||
});
|
||||
@ -81,7 +84,6 @@ const ScheduledCommandEdit = () => {
|
||||
|
||||
navigate("/server_actions");
|
||||
} catch (error) {
|
||||
console.log("Error saving scheduled command:", error);
|
||||
notify("scheduled_commands.action.update_failure", { type: "error" });
|
||||
}
|
||||
};
|
||||
|
@ -4,6 +4,8 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
Loading,
|
||||
Button,
|
||||
useDataProvider,
|
||||
useNotify,
|
||||
SimpleShowLayout,
|
||||
TextField,
|
||||
BooleanField,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
|
||||
import ScheduledDeleteButton from "./ScheduledDeleteButton";
|
||||
import { useAppContext } from "../../../../../Context";
|
||||
import { ScheduledCommand } from "../../../../../synapse/dataProvider";
|
||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { Paper } from "@mui/material";
|
||||
import { Loading, Button } from "react-admin";
|
||||
import { Loading, Button, useNotify, useRefresh, useCreatePath, useRecordContext } from "react-admin";
|
||||
import { ResourceContextProvider, useList } from "react-admin";
|
||||
import { ListContextProvider, TextField } from "react-admin";
|
||||
import { Datagrid } from "react-admin";
|
||||
import { BooleanField, DateField, TopToolbar } from "react-admin";
|
||||
import { useDataProvider } from "react-admin";
|
||||
import { Identifier } from "react-admin";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAppContext } from "../../../../../Context";
|
||||
import { DATE_FORMAT } from "../../../../../utils/date";
|
||||
import { useScheduledCommands } from "../../hooks/useScheduledCommands";
|
||||
const ListActions = () => {
|
||||
@ -25,7 +27,7 @@ const ListActions = () => {
|
||||
};
|
||||
|
||||
const ScheduledCommandsList = () => {
|
||||
const { data, isLoading } = useScheduledCommands();
|
||||
const { data, isLoading, error } = useScheduledCommands();
|
||||
|
||||
const listContext = useList({
|
||||
resource: "scheduled",
|
||||
@ -44,7 +46,6 @@ const ScheduledCommandsList = () => {
|
||||
<Paper>
|
||||
<Datagrid
|
||||
bulkActionButtons={false}
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
rowClick={(id: Identifier, resource: string, record: any) => {
|
||||
if (!record) {
|
||||
return "";
|
||||
|
@ -7,7 +7,16 @@ import DownloadingIcon from "@mui/icons-material/Downloading";
|
||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
|
||||
import {
|
||||
Grid2 as Grid,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { get } from "lodash";
|
||||
@ -140,6 +149,7 @@ const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
};
|
||||
|
||||
export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const notify = useNotify();
|
||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||
|
@ -3,7 +3,7 @@ import { CardContent, CardHeader, Container } from "@mui/material";
|
||||
import { useTranslate } from "ra-core";
|
||||
import { ChangeEventHandler } from "react";
|
||||
|
||||
import { ImportResult, ParsedStats, Progress } from "./types";
|
||||
import { ParsedStats, Progress } from "./types";
|
||||
|
||||
const TranslatableOption = ({ value, text }: { value: string; text: string }) => {
|
||||
const translate = useTranslate();
|
||||
@ -18,7 +18,7 @@ const ConflictModeCard = ({
|
||||
progress,
|
||||
}: {
|
||||
stats: ParsedStats | null;
|
||||
importResults: ImportResult | null;
|
||||
importResults: any;
|
||||
onConflictModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
||||
conflictMode: string;
|
||||
progress: Progress;
|
||||
|
@ -5,7 +5,7 @@ import { Checkbox } from "@mui/material";
|
||||
import { useTranslate } from "ra-core";
|
||||
import { ChangeEventHandler } from "react";
|
||||
|
||||
import { ImportResult, ParsedStats, Progress } from "./types";
|
||||
import { ParsedStats, Progress } from "./types";
|
||||
|
||||
const StatsCard = ({
|
||||
stats,
|
||||
@ -18,7 +18,7 @@ const StatsCard = ({
|
||||
}: {
|
||||
stats: ParsedStats | null;
|
||||
progress: Progress;
|
||||
importResults: ImportResult | null;
|
||||
importResults: any;
|
||||
useridMode: string;
|
||||
passwordMode: boolean;
|
||||
onUseridModeChanged: ChangeEventHandler<HTMLSelectElement>;
|
||||
|
@ -2,14 +2,14 @@ import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } fr
|
||||
import { useTranslate } from "ra-core";
|
||||
import { ChangeEventHandler } from "react";
|
||||
|
||||
import { ImportResult, Progress } from "./types";
|
||||
import { Progress } from "./types";
|
||||
|
||||
const UploadCard = ({
|
||||
importResults,
|
||||
onFileChange,
|
||||
progress,
|
||||
}: {
|
||||
importResults: ImportResult | null;
|
||||
importResults: any;
|
||||
onFileChange: ChangeEventHandler<HTMLInputElement>;
|
||||
progress: Progress;
|
||||
}) => {
|
||||
|
@ -273,7 +273,7 @@ const useImportFile = () => {
|
||||
let retries = 0;
|
||||
const submitRecord = async (recordData: ImportLine) => {
|
||||
try {
|
||||
await dataProvider.getOne("users", { id: recordData.id });
|
||||
const response = await dataProvider.getOne("users", { id: recordData.id });
|
||||
|
||||
if (LOGGING) console.log("already existed");
|
||||
|
||||
|
@ -1,13 +1,3 @@
|
||||
// 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 { SynapseTranslationMessages } from ".";
|
||||
|
519
src/i18n/ja.ts
519
src/i18n/ja.ts
@ -1,519 +0,0 @@
|
||||
// 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;
|
@ -1,4 +1,4 @@
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||
import { AdminContext } from "react-admin";
|
||||
|
||||
@ -7,6 +7,7 @@ import { AppContext } from "../Context";
|
||||
import englishMessages from "../i18n/en";
|
||||
|
||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||
import { act } from "@testing-library/react";
|
||||
|
||||
describe("LoginForm", () => {
|
||||
it("renders with no restriction to homeserver", async () => {
|
||||
|
@ -161,14 +161,14 @@ const LoginPage = () => {
|
||||
try {
|
||||
const serverVersion = await getServerVersion(url);
|
||||
setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setServerVersion("");
|
||||
}
|
||||
|
||||
try {
|
||||
const features = await getSupportedFeatures(url);
|
||||
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setMatrixVersions("");
|
||||
}
|
||||
|
||||
@ -179,7 +179,7 @@ const LoginPage = () => {
|
||||
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
||||
setSupportPassAuth(supportPass);
|
||||
setSSOBaseUrl(supportSSO ? url : "");
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setSupportPassAuth(false);
|
||||
setSSOBaseUrl("");
|
||||
}
|
||||
|
@ -114,6 +114,7 @@ const destinationFieldRender = (record: RaRecord) => {
|
||||
};
|
||||
|
||||
export const DestinationList = (props: ListProps) => {
|
||||
const record = useRecordContext(props);
|
||||
return (
|
||||
<List
|
||||
{...props}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
BooleanInput,
|
||||
Create,
|
||||
CreateProps,
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
DateTimeInput,
|
||||
|
@ -2,6 +2,7 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import ReportIcon from "@mui/icons-material/Warning";
|
||||
import {
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
DeleteButton,
|
||||
|
@ -119,9 +119,13 @@ export const MakeAdminBtn = () => {
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const result = await dataProvider.makeRoomAdmin(record.room_id, userIdValue);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
try {
|
||||
const result = await dataProvider.makeRoomAdmin(record.room_id, userIdValue);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
@ -199,6 +203,7 @@ export const MakeAdminBtn = () => {
|
||||
|
||||
export const RoomShow = (props: ShowProps) => {
|
||||
const translate = useTranslate();
|
||||
const record = useRecordContext();
|
||||
return (
|
||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||
<TabbedShowLayout>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import {
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
ExportButton,
|
||||
List,
|
||||
|
@ -219,12 +219,13 @@ export const UserList = (props: ListProps) => (
|
||||
// here only local part of user_id
|
||||
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||
// 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 UserEditActions = () => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
let ownUserIsSelected = false;
|
||||
let asManagedUserIsSelected = false;
|
||||
@ -261,7 +262,6 @@ export const UserCreate = (props: CreateProps) => {
|
||||
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(
|
||||
<Typography component="span"></Typography>
|
||||
);
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [create] = useCreate();
|
||||
|
||||
@ -284,7 +284,6 @@ export const UserCreate = (props: CreateProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const postSave = (data: Record<string, any>) => {
|
||||
setFormData(data);
|
||||
if (!userIsAvailable) {
|
||||
|
@ -17,7 +17,6 @@ import { GetConfig } from "../utils/config";
|
||||
import { MatrixError, displayError } from "../utils/error";
|
||||
import { returnMXID } from "../utils/mxid";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const CACHED_MANY_REF: Record<string, any> = {};
|
||||
|
||||
// Adds the access token to all requests
|
||||
@ -34,7 +33,7 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
||||
try {
|
||||
const response = await fetchUtils.fetchJson(url, options);
|
||||
return response;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
const errorBody = error.body as MatrixError;
|
||||
@ -46,11 +45,16 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-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 {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
@ -334,19 +338,6 @@ export interface RecurringCommand {
|
||||
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 {
|
||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
@ -375,8 +366,6 @@ export interface SynapseDataProvider extends DataProvider {
|
||||
createRecurringCommand: (etkeAdminUrl: string, command: Partial<RecurringCommand>) => Promise<RecurringCommand>;
|
||||
updateRecurringCommand: (etkeAdminUrl: string, command: RecurringCommand) => Promise<RecurringCommand>;
|
||||
deleteRecurringCommand: (etkeAdminUrl: string, id: string) => Promise<{ success: boolean }>;
|
||||
getPayments: (etkeAdminUrl: string) => Promise<PaymentsResponse>;
|
||||
getInvoice: (etkeAdminUrl: string, transactionId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const resourceMap = {
|
||||
@ -1001,7 +990,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
},
|
||||
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
||||
const filtered = Object.entries(rateLimits)
|
||||
.filter(([_key, value]) => value !== null && value !== undefined)
|
||||
.filter(([key, value]) => value !== null && value !== undefined)
|
||||
.reduce((obj, [key, value]) => {
|
||||
obj[key] = value;
|
||||
return obj;
|
||||
@ -1034,7 +1023,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
const endpoint_url = `${base_url}/_synapse/admin/v1/rooms/${encodeURIComponent(room_id)}/make_room_admin`;
|
||||
try {
|
||||
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ user_id }) });
|
||||
const { json } = await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ user_id }) });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
@ -1047,7 +1036,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
const base_url = localStorage.getItem("base_url");
|
||||
const endpoint_url = `${base_url}/_synapse/admin/v1/suspend/${encodeURIComponent(returnMXID(id))}`;
|
||||
try {
|
||||
await jsonClient(endpoint_url, {
|
||||
const { json } = await jsonClient(endpoint_url, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ suspend: suspendValue }),
|
||||
});
|
||||
@ -1222,7 +1211,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error("Error fetching server commands:", error);
|
||||
console.error("Error fetching server commands, error");
|
||||
}
|
||||
|
||||
return {};
|
||||
@ -1282,7 +1271,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching scheduled commands:", error);
|
||||
console.error("Error fetching scheduled commands, error");
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@ -1307,7 +1296,7 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching recurring commands:", error);
|
||||
console.error("Error fetching recurring commands, error");
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@ -1467,92 +1456,6 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
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, [
|
||||
@ -1598,7 +1501,7 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||
}
|
||||
return params;
|
||||
},
|
||||
beforeDelete: async (params: DeleteParams<any>, _dataProvider: DataProvider) => {
|
||||
beforeDelete: async (params: DeleteParams<any>, dataProvider: DataProvider) => {
|
||||
if (params.meta?.deleteMedia) {
|
||||
const base_url = localStorage.getItem("base_url");
|
||||
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
|
||||
@ -1613,7 +1516,7 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||
|
||||
return params;
|
||||
},
|
||||
beforeDeleteMany: async (params: DeleteManyParams<any>, _dataProvider: DataProvider) => {
|
||||
beforeDeleteMany: async (params: DeleteManyParams<any>, dataProvider: DataProvider) => {
|
||||
await Promise.all(
|
||||
params.ids.map(async id => {
|
||||
if (params.meta?.deleteMedia) {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { fetchUtils } from "react-admin";
|
||||
import { Identifier, fetchUtils } from "react-admin";
|
||||
|
||||
import { isMXID } from "../utils/mxid";
|
||||
|
||||
export const splitMxid = mxid => {
|
||||
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
||||
|
@ -69,7 +69,7 @@ export const FetchConfig = async () => {
|
||||
// load config from context
|
||||
// we deliberately processing each key separately to avoid overwriting the whole config, losing some keys, and messing
|
||||
// with typescript types
|
||||
export const LoadConfig = (context: object) => {
|
||||
export const LoadConfig = (context: any) => {
|
||||
if (context?.restrictBaseUrl) {
|
||||
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
* @returns The decoded string, or the original string if decoding fails.
|
||||
* @example decodeURIComponent("Hello%20World") // "Hello World"
|
||||
*/
|
||||
const decodeURLComponent = (str: string): string => {
|
||||
const decodeURLComponent = (str: any): any => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user