Compare commits
65 Commits
v0.11.1-et
...
v0.11.1-et
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/workflows/workflow.yml
vendored
4
.github/workflows/workflow.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
|||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
- name: Login to ghcr.io
|
- name: Login to ghcr.io
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv dist synapse-admin
|
mv dist synapse-admin
|
||||||
tar chvzf synapse-admin.tar.gz synapse-admin
|
tar chvzf synapse-admin.tar.gz synapse-admin
|
||||||
- uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
- uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||||
with:
|
with:
|
||||||
files: synapse-admin.tar.gz
|
files: synapse-admin.tar.gz
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ 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)
|
* 🧪 [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)
|
* 🔧 [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)
|
* 🛡️ [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)
|
||||||
|
|
||||||
#### exclusive for [etke.cc](https://etke.cc) customers
|
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||||
|
|
||||||
@@ -126,6 +127,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
|
||||||
|
|
||||||
|
|||||||
86
package.json
86
package.json
@@ -1,56 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "synapse-admin",
|
"name": "synapse-admin",
|
||||||
"version": "0.11.1",
|
"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.31.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@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.17",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^24.0.13",
|
||||||
"@types/papaparse": "^5.3.16",
|
"@types/papaparse": "^5.3.16",
|
||||||
"@types/react": "^19.1.6",
|
"@types/react": "^19.1.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||||
"@typescript-eslint/parser": "^8.32.0",
|
"@typescript-eslint/parser": "^8.34.1",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.30.1",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"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.1",
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.0.4",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^30.0.4",
|
||||||
"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.0",
|
||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.4.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.33.1",
|
"typescript-eslint": "^8.35.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.0.4",
|
||||||
"vite-plugin-version-mark": "^0.1.4"
|
"vite-plugin-version-mark": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bicstone/ra-language-japanese": "^5.6.3",
|
||||||
"@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": "^7.1.1",
|
"@mui/icons-material": "^7.2.0",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.2.0",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.0",
|
||||||
"@tanstack/react-query": "^5.80.6",
|
"@tanstack/react-query": "^5.81.5",
|
||||||
"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",
|
||||||
@@ -59,16 +79,16 @@
|
|||||||
"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.3",
|
"ra-language-french": "^5.9.1",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^5.4.4",
|
"ra-language-russian": "^5.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-admin": "^5.8.3",
|
"react-admin": "^5.9.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-is": "^19.1.0",
|
"react-is": "^19.1.0",
|
||||||
"react-router": "^7.6.0",
|
"react-router": "^7.6.0",
|
||||||
"react-router-dom": "^7.6.2",
|
"react-router-dom": "^7.6.3",
|
||||||
"ts-jest-mock-import-meta": "^1.3.0"
|
"ts-jest-mock-import-meta": "^1.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -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 |
11
src/App.tsx
11
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 (简体中文)" },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,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,
|
||||||
@@ -83,11 +84,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 +121,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 (
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ 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 }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ 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 }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,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 response = 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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -53,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();
|
||||||
@@ -78,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";
|
||||||
@@ -149,7 +140,6 @@ 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 dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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 ".";
|
||||||
|
|||||||
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;
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -161,14 +161,14 @@ const LoginPage = () => {
|
|||||||
try {
|
try {
|
||||||
const serverVersion = await getServerVersion(url);
|
const serverVersion = await getServerVersion(url);
|
||||||
setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`);
|
setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setServerVersion("");
|
setServerVersion("");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const features = await getSupportedFeatures(url);
|
const features = await getSupportedFeatures(url);
|
||||||
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`);
|
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setMatrixVersions("");
|
setMatrixVersions("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ const LoginPage = () => {
|
|||||||
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
||||||
setSupportPassAuth(supportPass);
|
setSupportPassAuth(supportPass);
|
||||||
setSSOBaseUrl(supportSSO ? url : "");
|
setSSOBaseUrl(supportSSO ? url : "");
|
||||||
} catch (error) {
|
} catch {
|
||||||
setSupportPassAuth(false);
|
setSupportPassAuth(false);
|
||||||
setSSOBaseUrl("");
|
setSSOBaseUrl("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -366,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 = {
|
||||||
@@ -990,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;
|
||||||
@@ -1023,7 +1034,7 @@ 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 };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
@@ -1036,7 +1047,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
const endpoint_url = `${base_url}/_synapse/admin/v1/suspend/${encodeURIComponent(returnMXID(id))}`;
|
const endpoint_url = `${base_url}/_synapse/admin/v1/suspend/${encodeURIComponent(returnMXID(id))}`;
|
||||||
try {
|
try {
|
||||||
const { json } = await jsonClient(endpoint_url, {
|
await jsonClient(endpoint_url, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({ suspend: suspendValue }),
|
body: JSON.stringify({ suspend: suspendValue }),
|
||||||
});
|
});
|
||||||
@@ -1211,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 {};
|
||||||
@@ -1271,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 [];
|
||||||
},
|
},
|
||||||
@@ -1296,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 [];
|
||||||
},
|
},
|
||||||
@@ -1456,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, [
|
||||||
@@ -1501,7 +1598,7 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
|||||||
}
|
}
|
||||||
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`;
|
||||||
@@ -1516,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]+)$/;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const FetchConfig = async () => {
|
|||||||
// load config from context
|
// load config from context
|
||||||
// we deliberately processing each key separately to avoid overwriting the whole config, losing 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user