mirror of
https://github.com/etkecc/synapse-admin.git
synced 2024-11-21 15:25:22 +03:00
Add User Rate Limits tab (#125)
* Add User Rate Limits tab * update readme
This commit is contained in:
parent
a04b24a5d5
commit
9adc13e722
13 changed files with 256 additions and 83 deletions
|
@ -89,6 +89,7 @@ with a proper manifest.json generation on build)
|
||||||
* Allow setting version using `SYNAPSE_ADMIN_VERSION` environment variable on build (if git is not available)
|
* Allow setting version using `SYNAPSE_ADMIN_VERSION` environment variable on build (if git is not available)
|
||||||
* [Add option to control user's experimental features](https://github.com/etkecc/synapse-admin/pull/111)
|
* [Add option to control user's experimental features](https://github.com/etkecc/synapse-admin/pull/111)
|
||||||
* [Add random password generation on user create/edit form](https://github.com/etkecc/synapse-admin/pull/123)
|
* [Add random password generation on user create/edit form](https://github.com/etkecc/synapse-admin/pull/123)
|
||||||
|
* [Add option to set user's rate limits](https://github.com/etkecc/synapse-admin/pull/125)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
|
|
|
@ -10,86 +10,86 @@ const experimentalFeaturesMap = {
|
||||||
msc3575: "enable experimental sliding sync support",
|
msc3575: "enable experimental sliding sync support",
|
||||||
};
|
};
|
||||||
const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boolean, updateFeature: (feature_name: string, feature_value: boolean) => void}) => {
|
const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boolean, updateFeature: (feature_name: string, feature_value: boolean) => void}) => {
|
||||||
const featureKey = props.featureKey;
|
const featureKey = props.featureKey;
|
||||||
const featureValue = props.featureValue;
|
const featureValue = props.featureValue;
|
||||||
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
|
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
|
||||||
const [checked, setChecked] = useState(featureValue);
|
const [checked, setChecked] = useState(featureValue);
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setChecked(event.target.checked);
|
setChecked(event.target.checked);
|
||||||
props.updateFeature(featureKey, event.target.checked);
|
props.updateFeature(featureKey, event.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Stack
|
return <Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={2}
|
spacing={2}
|
||||||
alignItems="start"
|
alignItems="start"
|
||||||
sx={{
|
sx={{
|
||||||
padding: 2,
|
padding: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch checked={checked} onChange={handleChange} />
|
<Switch checked={checked} onChange={handleChange} />
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: "medium",
|
fontWeight: "medium",
|
||||||
color: "text.primary"
|
color: "text.primary"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{featureKey}
|
{featureKey}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
>
|
>
|
||||||
{featureDescription}
|
{featureDescription}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExperimentalFeaturesList = () => {
|
export const ExperimentalFeaturesList = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const dataProvider = useDataProvider() as SynapseDataProvider;
|
const dataProvider = useDataProvider() as SynapseDataProvider;
|
||||||
const [features, setFeatures] = useState({});
|
const [features, setFeatures] = useState({});
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFeatures = async () => {
|
||||||
|
const features = await dataProvider.getFeatures(record.id);
|
||||||
|
setFeatures(features);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
fetchFeatures();
|
||||||
const fetchFeatures = async () => {
|
}, []);
|
||||||
const features = await dataProvider.getFeatures(record.id);
|
|
||||||
setFeatures(features);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFeatures();
|
const updateFeature = async (feature_name: string, feature_value: boolean) => {
|
||||||
}, []);
|
const updatedFeatures = {...features, [feature_name]: feature_value} as ExperimentalFeaturesModel;
|
||||||
|
setFeatures(updatedFeatures);
|
||||||
|
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
|
||||||
|
notify("ra.notification.updated", {
|
||||||
|
messageArgs: { smart_count: 1 },
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updateFeature = async (feature_name: string, feature_value: boolean) => {
|
return <>
|
||||||
const updatedFeatures = {...features, [feature_name]: feature_value} as ExperimentalFeaturesModel;
|
<Stack
|
||||||
setFeatures(updatedFeatures);
|
direction="column"
|
||||||
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
|
spacing={1}
|
||||||
notify("ra.notification.updated", {
|
>
|
||||||
messageArgs: { smart_count: 1 },
|
{Object.keys(features).map((featureKey: string) =>
|
||||||
type: "success",
|
<ExperimentalFeatureRow
|
||||||
});
|
key={featureKey}
|
||||||
};
|
featureKey={featureKey}
|
||||||
|
featureValue={features[featureKey]}
|
||||||
return <>
|
updateFeature={updateFeature}
|
||||||
<Stack
|
/>
|
||||||
direction="column"
|
)}
|
||||||
spacing={1}
|
</Stack>
|
||||||
>
|
</>
|
||||||
{Object.keys(features).map((featureKey: string) =>
|
|
||||||
<ExperimentalFeatureRow
|
|
||||||
key={featureKey}
|
|
||||||
featureKey={featureKey}
|
|
||||||
featureValue={features[featureKey]}
|
|
||||||
updateFeature={updateFeature}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
90
src/components/UserRateLimits.tsx
Normal file
90
src/components/UserRateLimits.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { Stack, Typography } from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
|
||||||
|
import { TextField } from "@mui/material";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value: number, updateRateLimit: (limit: string, value: number) => void }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateRateLimit(limit, parseInt(event.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Stack
|
||||||
|
spacing={1}
|
||||||
|
alignItems="start"
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
id="outlined-number"
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: {
|
||||||
|
shrink: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
label={translate(`resources.users.limits.${limit}`)}
|
||||||
|
/>
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
{translate(`resources.users.limits.${limit}_text`)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserRateLimits = () => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const notify = useNotify();
|
||||||
|
const record = useRecordContext();
|
||||||
|
const form = useFormContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const [rateLimits, setRateLimits] = useState({
|
||||||
|
messages_per_second: 0,
|
||||||
|
burst_count: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRateLimits = async () => {
|
||||||
|
const rateLimits = await dataProvider.getRateLimits(record.id);
|
||||||
|
if (Object.keys(rateLimits).length > 0) {
|
||||||
|
setRateLimits(rateLimits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRateLimits();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateRateLimit = async (limit: string, value: number) => {
|
||||||
|
let updatedRateLimits = { ...rateLimits, [limit]: value };
|
||||||
|
setRateLimits(updatedRateLimits);
|
||||||
|
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
{Object.keys(rateLimits).map((limit: string) =>
|
||||||
|
<RateLimitRow
|
||||||
|
key={limit}
|
||||||
|
limit={limit}
|
||||||
|
value={rateLimits[limit]}
|
||||||
|
updateRateLimit={updateRateLimit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
};
|
|
@ -55,7 +55,7 @@ const de: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Raumdetails",
|
details: "Raumdetails",
|
||||||
|
@ -192,6 +192,12 @@ const de: SynapseTranslationMessages = {
|
||||||
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
|
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
|
||||||
generate_password: "Passwort generieren",
|
generate_password: "Passwort generieren",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "Nachrichten pro Sekunde",
|
||||||
|
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können. 0 bedeutet, dass die Rate-Limitierung für diesen Benutzer deaktiviert ist.",
|
||||||
|
burst_count: "Burst-Anzahl",
|
||||||
|
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "Raum |||| Räume",
|
name: "Raum |||| Räume",
|
||||||
|
|
|
@ -25,7 +25,11 @@ const en: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
||||||
tabs: { sso: "SSO" },
|
tabs: {
|
||||||
|
sso: "SSO",
|
||||||
|
experimental: "Experimental",
|
||||||
|
limits: "Rate Limits",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Room details",
|
details: "Room details",
|
||||||
|
@ -161,6 +165,12 @@ const en: SynapseTranslationMessages = {
|
||||||
redact_events: "Redact all events sent by the user(-s)",
|
redact_events: "Redact all events sent by the user(-s)",
|
||||||
generate_password: "Generate password",
|
generate_password: "Generate password",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "Messages per second",
|
||||||
|
messages_per_second_text: "The number of actions that can be performed in a second. 0 mean that ratelimiting is disabled for this user",
|
||||||
|
burst_count: "Burst count",
|
||||||
|
burst_count_text: "How many actions that can be performed before being limited.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "Room |||| Rooms",
|
name: "Room |||| Rooms",
|
||||||
|
|
|
@ -24,7 +24,7 @@ const fa: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -157,6 +157,12 @@ const fa: SynapseTranslationMessages = {
|
||||||
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
|
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
|
||||||
generate_password: "توليد رمز عبور",
|
generate_password: "توليد رمز عبور",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "پیام در ثانیه",
|
||||||
|
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود. 0 به معنای غیرفعال کردن محدودیت برای این کاربر است.",
|
||||||
|
burst_count: "تعداد پیچیدگی",
|
||||||
|
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "اتاق |||| اتاق ها",
|
name: "اتاق |||| اتاق ها",
|
||||||
|
|
|
@ -24,7 +24,7 @@ const fr: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||||
tabs: { sso: "Authentification unique" },
|
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -159,6 +159,12 @@ const fr: SynapseTranslationMessages = {
|
||||||
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
|
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
|
||||||
generate_password: "Générer un mot de passe",
|
generate_password: "Générer un mot de passe",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "Messages par seconde",
|
||||||
|
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde. 0 signifie que la limitation est désactivée pour cet utilisateur.",
|
||||||
|
burst_count: "Compteur de pics",
|
||||||
|
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "Salon |||| Salons",
|
name: "Salon |||| Salons",
|
||||||
|
|
8
src/i18n/index.d.ts
vendored
8
src/i18n/index.d.ts
vendored
|
@ -22,7 +22,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: string;
|
invalid_user_id: string;
|
||||||
tabs: { sso: string };
|
tabs: { sso: string; experimental: string; limits: string; };
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
details?: string; // TODO: fa, fr, it, zh
|
details?: string; // TODO: fa, fr, it, zh
|
||||||
|
@ -157,6 +157,12 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||||
redact_events: string;
|
redact_events: string;
|
||||||
generate_password: string;
|
generate_password: string;
|
||||||
};
|
};
|
||||||
|
limits: {
|
||||||
|
messages_per_second: string;
|
||||||
|
messages_per_second_text: string;
|
||||||
|
burst_count: string;
|
||||||
|
burst_count_text: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -24,7 +24,7 @@ const it: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO", experimental: "Sperimentale", limits: "Limiti" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -158,6 +158,12 @@ const it: SynapseTranslationMessages = {
|
||||||
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
|
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
|
||||||
generate_password: "Genera password",
|
generate_password: "Genera password",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "Messaggi al secondo",
|
||||||
|
messages_per_second_text: "Il numero di azioni che l'utente può eseguire al secondo. 0 significa che la limitazione è disabilitata per questo utente.",
|
||||||
|
burst_count: "Burst-conteggio",
|
||||||
|
burst_count_text: "Il numero di azioni che l'utente può eseguire prima di essere limitato.",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "Stanza |||| Stanze",
|
name: "Stanza |||| Stanze",
|
||||||
|
|
|
@ -50,7 +50,7 @@ const ru: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Данные комнаты",
|
details: "Данные комнаты",
|
||||||
|
@ -195,7 +195,13 @@ const ru: SynapseTranslationMessages = {
|
||||||
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
|
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
|
||||||
generate_password: "Сгенерировать пароль",
|
generate_password: "Сгенерировать пароль",
|
||||||
},
|
},
|
||||||
},
|
limits: {
|
||||||
|
messages_per_second: "Сообщений в секунду",
|
||||||
|
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду. 0 означает, что ограничение на количество действий отключено для этого пользователя.",
|
||||||
|
burst_count: "Burst-счётчик",
|
||||||
|
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
|
||||||
|
}
|
||||||
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "Комната |||| Комнаты",
|
name: "Комната |||| Комнаты",
|
||||||
fields: {
|
fields: {
|
||||||
|
|
|
@ -52,7 +52,7 @@ const zh: SynapseTranslationMessages = {
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
tabs: { sso: "SSO" },
|
tabs: { sso: "SSO", experimental: "实验性", limits: "限制" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -182,6 +182,12 @@ const zh: SynapseTranslationMessages = {
|
||||||
redact_events: "重新编辑用户(-s)发送的所有事件",
|
redact_events: "重新编辑用户(-s)发送的所有事件",
|
||||||
generate_password: "生成密码",
|
generate_password: "生成密码",
|
||||||
},
|
},
|
||||||
|
limits: {
|
||||||
|
messages_per_second: "每秒消息数",
|
||||||
|
messages_per_second_text: "每秒可以执行的操作数。0 表示禁用此用户的限制。",
|
||||||
|
burst_count: "Burst-计数",
|
||||||
|
burst_count_text: "在限制之前可以执行的操作数。",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
name: "房间",
|
name: "房间",
|
||||||
|
|
|
@ -8,9 +8,10 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||||||
import ScienceIcon from "@mui/icons-material/Science";
|
import ScienceIcon from "@mui/icons-material/Science";
|
||||||
|
import LockClockIcon from '@mui/icons-material/LockClock';
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert, Switch, Stack, Typography } from "@mui/material";
|
import { Alert } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
ArrayInput,
|
ArrayInput,
|
||||||
ArrayField,
|
ArrayField,
|
||||||
|
@ -72,6 +73,7 @@ import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../comp
|
||||||
import { generateRandomPassword } from "../synapse/synapse";
|
import { generateRandomPassword } from "../synapse/synapse";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { ExperimentalFeaturesList } from "../components/ExperimentalFeatures";
|
import { ExperimentalFeaturesList } from "../components/ExperimentalFeatures";
|
||||||
|
import { UserRateLimits } from "../components/UserRateLimits";
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
|
@ -476,9 +478,13 @@ export const UserEdit = (props: EditProps) => {
|
||||||
</ReferenceManyField>
|
</ReferenceManyField>
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
<FormTab label="Experimental" icon={<ScienceIcon />} path="experimental">
|
<FormTab label="synapseadmin.users.tabs.experimental" icon={<ScienceIcon />} path="experimental">
|
||||||
<ExperimentalFeaturesList />
|
<ExperimentalFeaturesList />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
|
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
||||||
|
<UserRateLimits />
|
||||||
|
</FormTab>
|
||||||
</TabbedForm>
|
</TabbedForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|
|
@ -254,10 +254,17 @@ export interface ExperimentalFeaturesModel {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RateLimitsModel {
|
||||||
|
messages_per_second?: number;
|
||||||
|
burst_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
||||||
|
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
|
||||||
|
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
|
@ -816,6 +823,17 @@ const baseDataProvider: SynapseDataProvider = {
|
||||||
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
|
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
|
||||||
await jsonClient(endpoint_url, { method: "PUT", body: JSON.stringify({ features }) });
|
await jsonClient(endpoint_url, { method: "PUT", body: JSON.stringify({ features }) });
|
||||||
},
|
},
|
||||||
|
getRateLimits: async (id: Identifier) => {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
|
||||||
|
const { json } = await jsonClient(endpoint_url);
|
||||||
|
return json as RateLimitsModel;
|
||||||
|
},
|
||||||
|
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
|
||||||
|
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(rateLimits) });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
|
@ -824,6 +842,12 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
|
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
|
||||||
const avatarFile = params.data.avatar_file?.rawFile;
|
const avatarFile = params.data.avatar_file?.rawFile;
|
||||||
const avatarErase = params.data.avatar_erase;
|
const avatarErase = params.data.avatar_erase;
|
||||||
|
const rates = params.data.rates;
|
||||||
|
|
||||||
|
if (rates) {
|
||||||
|
await dataProvider.setRateLimits(params.id, rates);
|
||||||
|
delete params.data.rates;
|
||||||
|
}
|
||||||
|
|
||||||
if (avatarErase) {
|
if (avatarErase) {
|
||||||
params.data.avatar_url = "";
|
params.data.avatar_url = "";
|
||||||
|
|
Loading…
Reference in a new issue