Transform code base to typescript
Change-Id: Ia1f862fb5962ddd54b8d7643abbc39bb314d1f8e
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Admin,
|
||||
CustomRoutes,
|
||||
@@ -6,7 +5,7 @@ import {
|
||||
resolveBrowserLocale,
|
||||
} from "react-admin";
|
||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||
import merge from "lodash/merge";
|
||||
import { merge } from "lodash";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
import users from "./components/users";
|
@@ -1,5 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
9
src/AppContext.tsx
Normal file
9
src/AppContext.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface AppContextType {
|
||||
restrictBaseUrl: string | string[];
|
||||
}
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext) as AppContextType;
|
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { RecordContextProvider } from "react-admin";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import AvatarField from "./AvatarField";
|
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import get from "lodash/get";
|
||||
import { get } from "lodash";
|
||||
import { Avatar } from "@mui/material";
|
||||
import { useRecordContext } from "react-admin";
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Datagrid,
|
||||
DateField,
|
||||
DeleteButton,
|
||||
List,
|
||||
ListProps,
|
||||
NumberField,
|
||||
Pagination,
|
||||
ReferenceField,
|
||||
ResourceProps,
|
||||
Show,
|
||||
ShowProps,
|
||||
Tab,
|
||||
TabbedShowLayout,
|
||||
TextField,
|
||||
@@ -20,7 +22,7 @@ import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import ReportIcon from "@mui/icons-material/Warning";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
|
||||
const date_format = {
|
||||
const date_format: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
@@ -33,7 +35,7 @@ const ReportPagination = () => (
|
||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||
);
|
||||
|
||||
export const ReportShow = props => {
|
||||
export const ReportShow = (props: ShowProps) => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Show {...props} actions={<ReportShowActions />}>
|
||||
@@ -120,7 +122,7 @@ const ReportShowActions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ReportList = props => (
|
||||
export const ReportList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
pagination={<ReportPagination />}
|
||||
@@ -141,7 +143,7 @@ export const ReportList = props => (
|
||||
</List>
|
||||
);
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "reports",
|
||||
icon: ReportIcon,
|
||||
list: ReportList,
|
@@ -1,6 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDataProvider, useNotify, Title } from "react-admin";
|
||||
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
||||
import {
|
||||
parse as parseCsv,
|
||||
unparse as unparseCsv,
|
||||
ParseResult,
|
||||
} from "papaparse";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -12,36 +16,63 @@ import {
|
||||
FormControlLabel,
|
||||
NativeSelect,
|
||||
} from "@mui/material";
|
||||
import { useTranslate } from "ra-core";
|
||||
import { DataProvider, useTranslate } from "ra-core";
|
||||
import { generateRandomUser } from "./users";
|
||||
|
||||
const LOGGING = true;
|
||||
|
||||
const expectedFields = ["id", "displayname"].sort();
|
||||
const optionalFields = [
|
||||
"user_type",
|
||||
"guest",
|
||||
"admin",
|
||||
"deactivated",
|
||||
"avatar_url",
|
||||
"password",
|
||||
].sort();
|
||||
|
||||
function TranslatableOption({ value, text }) {
|
||||
const translate = useTranslate();
|
||||
return <option value={value}>{translate(text)}</option>;
|
||||
}
|
||||
|
||||
type Progress = {
|
||||
done: number;
|
||||
limit: number;
|
||||
} | null;
|
||||
|
||||
interface ImportLine {
|
||||
id: string;
|
||||
displayname: string;
|
||||
user_type?: string;
|
||||
name?: string;
|
||||
deactivated?: boolean;
|
||||
guest?: boolean;
|
||||
admin?: boolean;
|
||||
is_admin?: boolean;
|
||||
password?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
interface ChangeStats {
|
||||
total: number;
|
||||
id: number;
|
||||
is_guest: number;
|
||||
admin: number;
|
||||
password: number;
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
skippedRecords: RaRecord[];
|
||||
erroredRecords: RaRecord[];
|
||||
succeededRecords: RaRecord[];
|
||||
totalRecordCount: number;
|
||||
changeStats: ChangeStats;
|
||||
wasDryRun: boolean;
|
||||
}
|
||||
|
||||
const FilePicker = () => {
|
||||
const [values, setValues] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [values, setValues] = useState<ImportLine[]>([]);
|
||||
const [error, setError] = useState<string | string[] | null>(null);
|
||||
const [stats, setStats] = useState<ChangeStats | null>(null);
|
||||
const [dryRun, setDryRun] = useState(true);
|
||||
|
||||
const [progress, setProgress] = useState(null);
|
||||
const [progress, setProgress] = useState<Progress>(null);
|
||||
|
||||
const [importResults, setImportResults] = useState(null);
|
||||
const [skippedRecords, setSkippedRecords] = useState(null);
|
||||
const [importResults, setImportResults] = useState<ImportResult | null>(null);
|
||||
const [skippedRecords, setSkippedRecords] = useState<string>("");
|
||||
|
||||
const [conflictMode, setConflictMode] = useState("stop");
|
||||
const [passwordMode, setPasswordMode] = useState(true);
|
||||
@@ -52,14 +83,15 @@ const FilePicker = () => {
|
||||
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
const onFileChange = async e => {
|
||||
const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (progress !== null) return;
|
||||
|
||||
setValues(null);
|
||||
setValues([]);
|
||||
setError(null);
|
||||
setStats(null);
|
||||
setImportResults(null);
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
if (!file) return;
|
||||
/* Let's refuse some unreasonably big files instead of freezing
|
||||
* up the browser */
|
||||
if (file.size > 100000000) {
|
||||
@@ -71,12 +103,12 @@ const FilePicker = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
parseCsv(file, {
|
||||
parseCsv<ImportLine>(file, {
|
||||
header: true,
|
||||
skipEmptyLines: true /* especially for a final EOL in the csv file */,
|
||||
complete: result => {
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
if (result.errors) {
|
||||
setError(result.errors.map(e => e.toString()));
|
||||
}
|
||||
/* Papaparse is very lenient, we may be able to salvage
|
||||
* the data in the file. */
|
||||
@@ -84,31 +116,25 @@ const FilePicker = () => {
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
setError(true);
|
||||
setError("Unknown error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyCsv = (
|
||||
{ data, meta, errors },
|
||||
{ data, meta, errors }: ParseResult<ImportLine>,
|
||||
{ setValues, setStats, setError }
|
||||
) => {
|
||||
/* First, verify the presence of required fields */
|
||||
let eF = Array.from(expectedFields);
|
||||
let oF = Array.from(optionalFields);
|
||||
const missingFields = expectedFields.filter(eF =>
|
||||
meta.fields?.find(mF => eF === mF)
|
||||
);
|
||||
|
||||
meta.fields.forEach(name => {
|
||||
if (eF.includes(name)) {
|
||||
eF = eF.filter(v => v !== name);
|
||||
}
|
||||
if (oF.includes(name)) {
|
||||
oF = oF.filter(v => v !== name);
|
||||
}
|
||||
});
|
||||
|
||||
if (eF.length !== 0) {
|
||||
if (missingFields.length > 0) {
|
||||
setError(
|
||||
translate("import_users.error.required_field", { field: eF[0] })
|
||||
translate("import_users.error.required_field", {
|
||||
field: missingFields[0],
|
||||
})
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -119,7 +145,7 @@ const FilePicker = () => {
|
||||
/* Collect some stats to prevent sneaky csv files from adding admin
|
||||
users or something.
|
||||
*/
|
||||
let stats = {
|
||||
const stats = {
|
||||
user_types: { default: 0 },
|
||||
is_guest: 0,
|
||||
admin: 0,
|
||||
@@ -131,6 +157,7 @@ const FilePicker = () => {
|
||||
total: data.length,
|
||||
};
|
||||
|
||||
var errorMessages = errors.map(e => e.message);
|
||||
data.forEach((line, idx) => {
|
||||
if (line.user_type === undefined || line.user_type === "") {
|
||||
stats.user_types.default++;
|
||||
@@ -141,14 +168,13 @@ const FilePicker = () => {
|
||||
* resource so it gives sensible field names and doesn't duplicate
|
||||
* id as "name"?
|
||||
*/
|
||||
if (meta.fields.includes("name")) {
|
||||
if (meta.fields?.includes("name")) {
|
||||
delete line.name;
|
||||
}
|
||||
if (meta.fields.includes("user_type")) {
|
||||
if (meta.fields?.includes("user_type")) {
|
||||
delete line.user_type;
|
||||
}
|
||||
if (meta.fields.includes("is_admin")) {
|
||||
line.admin = line.is_admin;
|
||||
if (meta.fields?.includes("is_admin")) {
|
||||
delete line.is_admin;
|
||||
}
|
||||
|
||||
@@ -158,7 +184,7 @@ const FilePicker = () => {
|
||||
line[f] = true; // we need true booleans instead of strings
|
||||
} else {
|
||||
if (line[f] !== "false" && line[f] !== "") {
|
||||
errors.push(
|
||||
errorMessages.push(
|
||||
translate("import_users.error.invalid_value", {
|
||||
field: f,
|
||||
row: idx,
|
||||
@@ -182,8 +208,8 @@ const FilePicker = () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
setError(errors);
|
||||
if (errorMessages.length > 0) {
|
||||
setError(errorMessages);
|
||||
}
|
||||
setStats(stats);
|
||||
setValues(data);
|
||||
@@ -191,7 +217,7 @@ const FilePicker = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const runImport = async _e => {
|
||||
const runImport = async () => {
|
||||
if (progress !== null) {
|
||||
notify("import_users.errors.already_in_progress");
|
||||
return;
|
||||
@@ -220,26 +246,27 @@ const FilePicker = () => {
|
||||
// which doesn't look very good.
|
||||
|
||||
const doImport = async (
|
||||
dataProvider,
|
||||
data,
|
||||
conflictMode,
|
||||
passwordMode,
|
||||
useridMode,
|
||||
dryRun,
|
||||
setProgress,
|
||||
setError
|
||||
) => {
|
||||
let skippedRecords = [];
|
||||
let erroredRecords = [];
|
||||
let succeededRecords = [];
|
||||
let changeStats = {
|
||||
toAdmin: 0,
|
||||
toGuest: 0,
|
||||
toRegular: 0,
|
||||
replacedPassword: 0,
|
||||
dataProvider: DataProvider,
|
||||
data: ImportLine[],
|
||||
conflictMode: string,
|
||||
passwordMode: boolean,
|
||||
useridMode: string,
|
||||
dryRun: boolean,
|
||||
setProgress: (progress: Progress) => void,
|
||||
setError: (message: string) => void
|
||||
): Promise<ImportResult> => {
|
||||
const skippedRecords: ImportLine[] = [];
|
||||
const erroredRecords: ImportLine[] = [];
|
||||
const succeededRecords: ImportLine[] = [];
|
||||
const changeStats: ChangeStats = {
|
||||
total: 0,
|
||||
id: 0,
|
||||
is_guest: 0,
|
||||
admin: 0,
|
||||
password: 0,
|
||||
};
|
||||
let entriesDone = 0;
|
||||
let entriesCount = data.length;
|
||||
const entriesCount = data.length;
|
||||
try {
|
||||
setProgress({ done: entriesDone, limit: entriesCount });
|
||||
for (const entry of data) {
|
||||
@@ -305,9 +332,9 @@ const FilePicker = () => {
|
||||
"will check for existence of record " + JSON.stringify(userRecord)
|
||||
);
|
||||
let retries = 0;
|
||||
const submitRecord = recordData => {
|
||||
const submitRecord = (recordData: ImportLine) => {
|
||||
return dataProvider.getOne("users", { id: recordData.id }).then(
|
||||
async _alreadyExists => {
|
||||
async () => {
|
||||
if (LOGGING) console.log("already existed");
|
||||
|
||||
if (useridMode === "update" || conflictMode === "skip") {
|
||||
@@ -332,7 +359,7 @@ const FilePicker = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
async _okToSubmit => {
|
||||
async () => {
|
||||
if (LOGGING)
|
||||
console.log(
|
||||
"OK to create record " +
|
||||
@@ -360,7 +387,7 @@ const FilePicker = () => {
|
||||
setError(
|
||||
translate("import_users.error.at_entry", {
|
||||
entry: entriesDone + 1,
|
||||
message: e.message,
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
setProgress(null);
|
||||
@@ -387,7 +414,7 @@ const FilePicker = () => {
|
||||
element.click();
|
||||
};
|
||||
|
||||
const onConflictModeChanged = async e => {
|
||||
const onConflictModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (progress !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -396,7 +423,7 @@ const FilePicker = () => {
|
||||
setConflictMode(value);
|
||||
};
|
||||
|
||||
const onPasswordModeChange = e => {
|
||||
const onPasswordModeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (progress !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -404,7 +431,7 @@ const FilePicker = () => {
|
||||
setPasswordMode(e.target.checked);
|
||||
};
|
||||
|
||||
const onUseridModeChanged = async e => {
|
||||
const onUseridModeChanged = async (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (progress !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -413,11 +440,11 @@ const FilePicker = () => {
|
||||
setUseridMode(value);
|
||||
};
|
||||
|
||||
const onDryRunModeChanged = ev => {
|
||||
const onDryRunModeChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (progress !== null) {
|
||||
return;
|
||||
}
|
||||
setDryRun(ev.target.checked);
|
||||
setDryRun(e.target.checked);
|
||||
};
|
||||
|
||||
// render individual small components
|
||||
@@ -462,7 +489,7 @@ const FilePicker = () => {
|
||||
<NativeSelect
|
||||
onChange={onUseridModeChanged}
|
||||
value={useridMode}
|
||||
enabled={(progress !== null).toString()}
|
||||
disabled={progress !== null}
|
||||
>
|
||||
<TranslatableOption
|
||||
value="ignore"
|
||||
@@ -496,7 +523,7 @@ const FilePicker = () => {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={passwordMode}
|
||||
enabled={(progress !== null).toString()}
|
||||
disabled={progress !== null}
|
||||
onChange={onPasswordModeChange}
|
||||
/>
|
||||
}
|
||||
@@ -510,7 +537,7 @@ const FilePicker = () => {
|
||||
</Container>,
|
||||
];
|
||||
|
||||
let conflictCards = stats && !importResults && (
|
||||
const conflictCards = stats && !importResults && (
|
||||
<Container>
|
||||
<CardHeader title={translate("import_users.cards.conflicts.header")} />
|
||||
<CardContent>
|
||||
@@ -518,7 +545,7 @@ const FilePicker = () => {
|
||||
<NativeSelect
|
||||
onChange={onConflictModeChanged}
|
||||
value={conflictMode}
|
||||
enabled={(progress !== null).toString()}
|
||||
disabled={progress !== null}
|
||||
>
|
||||
<TranslatableOption
|
||||
value="stop"
|
||||
@@ -534,7 +561,7 @@ const FilePicker = () => {
|
||||
</Container>
|
||||
);
|
||||
|
||||
let errorCards = error && (
|
||||
const errorCards = error && (
|
||||
<Container>
|
||||
<CardHeader title={translate("import_users.error.error")} />
|
||||
<CardContent>
|
||||
@@ -545,7 +572,7 @@ const FilePicker = () => {
|
||||
</Container>
|
||||
);
|
||||
|
||||
let uploadCard = !importResults && (
|
||||
const uploadCard = !importResults && (
|
||||
<Container>
|
||||
<CardHeader title={translate("import_users.cards.upload.header")} />
|
||||
<CardContent>
|
||||
@@ -556,13 +583,13 @@ const FilePicker = () => {
|
||||
<input
|
||||
type="file"
|
||||
onChange={onFileChange}
|
||||
enabled={(progress !== null).toString()}
|
||||
disabled={progress !== null}
|
||||
/>
|
||||
</CardContent>
|
||||
</Container>
|
||||
);
|
||||
|
||||
let resultsCard = importResults && (
|
||||
const resultsCard = importResults && (
|
||||
<CardContent>
|
||||
<CardHeader title={translate("import_users.cards.results.header")} />
|
||||
<div>
|
||||
@@ -608,7 +635,7 @@ const FilePicker = () => {
|
||||
</CardContent>
|
||||
);
|
||||
|
||||
let startImportCard =
|
||||
const startImportCard =
|
||||
!values || values.length === 0 || importResults ? undefined : (
|
||||
<CardActions>
|
||||
<FormControlLabel
|
||||
@@ -616,16 +643,12 @@ const FilePicker = () => {
|
||||
<Checkbox
|
||||
checked={dryRun}
|
||||
onChange={onDryRunModeChanged}
|
||||
enabled={(progress !== null).toString()}
|
||||
disabled={progress !== null}
|
||||
/>
|
||||
}
|
||||
label={translate("import_users.cards.startImport.simulate_only")}
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={runImport}
|
||||
enabled={(progress !== null).toString()}
|
||||
>
|
||||
<Button size="large" onClick={runImport} disabled={progress !== null}>
|
||||
{translate("import_users.cards.startImport.run_import")}
|
||||
</Button>
|
||||
{progress !== null ? (
|
||||
@@ -636,7 +659,7 @@ const FilePicker = () => {
|
||||
</CardActions>
|
||||
);
|
||||
|
||||
let allCards = [];
|
||||
const allCards: JSX.Element[] = [];
|
||||
if (uploadCard) allCards.push(uploadCard);
|
||||
if (errorCards) allCards.push(errorCards);
|
||||
if (conflictCards) allCards.push(conflictCards);
|
||||
@@ -644,7 +667,7 @@ const FilePicker = () => {
|
||||
if (startImportCard) allCards.push(startImportCard);
|
||||
if (resultsCard) allCards.push(resultsCard);
|
||||
|
||||
let cardContainer = <Card>{allCards}</Card>;
|
||||
const cardContainer = <Card>{allCards}</Card>;
|
||||
|
||||
return [
|
||||
<Title defaultTitle={translate("import_users.title")} />,
|
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { AdminContext } from "react-admin";
|
||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Form,
|
||||
FormDataConsumer,
|
||||
@@ -183,7 +183,7 @@ const LoginPage = () => {
|
||||
const [serverVersion, setServerVersion] = useState("");
|
||||
const [matrixVersions, setMatrixVersions] = useState("");
|
||||
|
||||
const handleUsernameChange = _ => {
|
||||
const handleUsernameChange = () => {
|
||||
if (formData.base_url || allowSingleBaseUrl) return;
|
||||
// check if username is a full qualified userId then set base_url accordingly
|
||||
const domain = splitMxid(formData.username)?.domain;
|
||||
@@ -238,7 +238,7 @@ const LoginPage = () => {
|
||||
<Box>
|
||||
<TextInput
|
||||
autoFocus
|
||||
name="username"
|
||||
source="username"
|
||||
label="ra.auth.username"
|
||||
autoComplete="username"
|
||||
disabled={loading || !supportPassAuth}
|
||||
@@ -250,7 +250,7 @@ const LoginPage = () => {
|
||||
</Box>
|
||||
<Box>
|
||||
<PasswordInput
|
||||
name="password"
|
||||
source="password"
|
||||
label="ra.auth.password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
@@ -262,7 +262,7 @@ const LoginPage = () => {
|
||||
</Box>
|
||||
<Box>
|
||||
<TextInput
|
||||
name="base_url"
|
||||
source="base_url"
|
||||
label="synapseadmin.auth.base_url"
|
||||
select={allowMultipleBaseUrls}
|
||||
autoComplete="url"
|
@@ -1,17 +1,20 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BooleanInput,
|
||||
Create,
|
||||
CreateProps,
|
||||
Datagrid,
|
||||
DateField,
|
||||
DateTimeInput,
|
||||
Edit,
|
||||
EditProps,
|
||||
List,
|
||||
ListProps,
|
||||
maxValue,
|
||||
number,
|
||||
NumberField,
|
||||
NumberInput,
|
||||
regex,
|
||||
ResourceProps,
|
||||
SaveButton,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
@@ -56,7 +59,7 @@ const dateFormatter = v => {
|
||||
|
||||
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
|
||||
|
||||
export const RegistrationTokenList = props => (
|
||||
export const RegistrationTokenList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
filters={registrationTokenFilters}
|
||||
@@ -79,7 +82,7 @@ export const RegistrationTokenList = props => (
|
||||
</List>
|
||||
);
|
||||
|
||||
export const RegistrationTokenCreate = props => (
|
||||
export const RegistrationTokenCreate = (props: CreateProps) => (
|
||||
<Create {...props} redirect="list">
|
||||
<SimpleForm
|
||||
toolbar={
|
||||
@@ -111,7 +114,7 @@ export const RegistrationTokenCreate = props => (
|
||||
</Create>
|
||||
);
|
||||
|
||||
export const RegistrationTokenEdit = props => (
|
||||
export const RegistrationTokenEdit = (props: EditProps) => (
|
||||
<Edit {...props}>
|
||||
<SimpleForm>
|
||||
<TextInput source="token" disabled />
|
||||
@@ -131,7 +134,7 @@ export const RegistrationTokenEdit = props => (
|
||||
</Edit>
|
||||
);
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "registration_tokens",
|
||||
icon: RegistrationTokenIcon,
|
||||
list: RegistrationTokenList,
|
@@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BooleanField,
|
||||
BulkDeleteButton,
|
||||
BulkDeleteButtonProps,
|
||||
Button,
|
||||
ButtonProps,
|
||||
DatagridConfigurable,
|
||||
DeleteButtonProps,
|
||||
ExportButton,
|
||||
DeleteButton,
|
||||
List,
|
||||
NumberField,
|
||||
Pagination,
|
||||
ResourceProps,
|
||||
SelectColumnsButton,
|
||||
TextField,
|
||||
TopToolbar,
|
||||
@@ -29,7 +32,7 @@ const RoomDirectoryPagination = () => (
|
||||
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
|
||||
);
|
||||
|
||||
export const RoomDirectoryUnpublishButton = props => {
|
||||
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
@@ -50,7 +53,9 @@ export const RoomDirectoryUnpublishButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RoomDirectoryBulkUnpublishButton = props => (
|
||||
export const RoomDirectoryBulkUnpublishButton = (
|
||||
props: BulkDeleteButtonProps
|
||||
) => (
|
||||
<BulkDeleteButton
|
||||
{...props}
|
||||
label="resources.room_directory.action.erase"
|
||||
@@ -62,7 +67,7 @@ export const RoomDirectoryBulkUnpublishButton = props => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const RoomDirectoryBulkPublishButton = props => {
|
||||
export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||
const { selectedIds } = useListContext();
|
||||
const notify = useNotify();
|
||||
const refresh = useRefresh();
|
||||
@@ -99,7 +104,7 @@ export const RoomDirectoryBulkPublishButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RoomDirectoryPublishButton = props => {
|
||||
export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
||||
const record = useRecordContext();
|
||||
const notify = useNotify();
|
||||
const refresh = useRefresh();
|
||||
@@ -148,7 +153,7 @@ export const RoomDirectoryList = () => (
|
||||
actions={<RoomDirectoryListActions />}
|
||||
>
|
||||
<DatagridConfigurable
|
||||
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
|
||||
rowClick={id => "/rooms/" + id + "/show"}
|
||||
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
|
||||
omit={["room_id", "canonical_alias", "topic"]}
|
||||
>
|
||||
@@ -197,7 +202,7 @@ export const RoomDirectoryList = () => (
|
||||
</List>
|
||||
);
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "room_directory",
|
||||
icon: RoomDirectoryIcon,
|
||||
list: RoomDirectoryList,
|
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
RaRecord,
|
||||
SaveButton,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarProps,
|
||||
required,
|
||||
useCreate,
|
||||
useDataProvider,
|
||||
@@ -24,10 +26,12 @@ import {
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
|
||||
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
const ServerNoticeToolbar = props => (
|
||||
const ServerNoticeToolbar = (
|
||||
props: ToolbarProps & { pristine?: boolean }
|
||||
) => (
|
||||
<Toolbar {...props}>
|
||||
<SaveButton
|
||||
label="resources.servernotices.action.send"
|
||||
@@ -40,7 +44,7 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{translate("resources.servernotices.action.send")}
|
||||
</DialogTitle>
|
||||
@@ -68,12 +72,12 @@ export const ServerNoticeButton = () => {
|
||||
const record = useRecordContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
const notify = useNotify();
|
||||
const [create, { isloading }] = useCreate();
|
||||
const [create, { isLoading }] = useCreate();
|
||||
|
||||
const handleDialogOpen = () => setOpen(true);
|
||||
const handleDialogClose = () => setOpen(false);
|
||||
|
||||
const handleSend = values => {
|
||||
const handleSend = (values: Partial<RaRecord>) => {
|
||||
create(
|
||||
"servernotices",
|
||||
{ data: { id: record.id, ...values } },
|
||||
@@ -95,7 +99,7 @@ export const ServerNoticeButton = () => {
|
||||
<Button
|
||||
label="resources.servernotices.send"
|
||||
onClick={handleDialogOpen}
|
||||
disabled={isloading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<MessageIcon />
|
||||
</Button>
|
30
src/components/date.ts
Normal file
30
src/components/date.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const DATE_FORMAT: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
export const dateParser = (v: string | number | Date): number => {
|
||||
const d = new Date(v);
|
||||
return d.getTime();
|
||||
};
|
||||
|
||||
export const dateFormatter = (
|
||||
v: string | number | Date | undefined | null
|
||||
): string => {
|
||||
if (v === undefined || v === null) return "";
|
||||
const d = new Date(v);
|
||||
|
||||
const pad = "00";
|
||||
const year = d.getFullYear().toString();
|
||||
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
|
||||
const day = (pad + d.getDate().toString()).slice(-2);
|
||||
const hour = (pad + d.getHours().toString()).slice(-2);
|
||||
const minute = (pad + d.getMinutes().toString()).slice(-2);
|
||||
|
||||
// target format yyyy-MM-ddThh:mm
|
||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||
};
|
@@ -1,14 +1,23 @@
|
||||
import React from "react";
|
||||
import { MouseEvent } from "react";
|
||||
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import {
|
||||
Button,
|
||||
Datagrid,
|
||||
DateField,
|
||||
List,
|
||||
ListProps,
|
||||
Pagination,
|
||||
RaRecord,
|
||||
ReferenceField,
|
||||
ReferenceManyField,
|
||||
ResourceProps,
|
||||
SearchInput,
|
||||
Show,
|
||||
ShowProps,
|
||||
Tab,
|
||||
TabbedShowLayout,
|
||||
TextField,
|
||||
@@ -19,25 +28,14 @@ import {
|
||||
useRefresh,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
|
||||
import { DATE_FORMAT } from "./date";
|
||||
|
||||
const DestinationPagination = () => (
|
||||
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||
);
|
||||
|
||||
const date_format = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const destinationRowSx = (record, _index) => ({
|
||||
const destinationRowSx = (record: RaRecord) => ({
|
||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
||||
});
|
||||
|
||||
@@ -52,7 +50,7 @@ export const DestinationReconnectButton = () => {
|
||||
// Reconnect is not required if no error has occurred. (`failure_ts`)
|
||||
if (!record || !record.failure_ts) return null;
|
||||
|
||||
const handleClick = e => {
|
||||
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
// Prevents redirection to the detail page when clicking in the list
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -100,7 +98,7 @@ const DestinationTitle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DestinationList = props => {
|
||||
export const DestinationList = (props: ListProps) => {
|
||||
return (
|
||||
<List
|
||||
{...props}
|
||||
@@ -110,12 +108,12 @@ export const DestinationList = props => {
|
||||
>
|
||||
<Datagrid
|
||||
rowSx={destinationRowSx}
|
||||
rowClick={(id, _resource, _record) => `${id}/show/rooms`}
|
||||
rowClick={id => `${id}/show/rooms`}
|
||||
bulkActionButtons={false}
|
||||
>
|
||||
<TextField source="destination" />
|
||||
<DateField source="failure_ts" showTime options={date_format} />
|
||||
<DateField source="retry_last_ts" showTime options={date_format} />
|
||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||
<TextField source="retry_interval" />
|
||||
<TextField source="last_successful_stream_ordering" />
|
||||
<DestinationReconnectButton />
|
||||
@@ -124,7 +122,7 @@ export const DestinationList = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DestinationShow = props => {
|
||||
export const DestinationShow = (props: ShowProps) => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Show
|
||||
@@ -135,8 +133,8 @@ export const DestinationShow = props => {
|
||||
<TabbedShowLayout>
|
||||
<Tab label="status" icon={<ViewListIcon />}>
|
||||
<TextField source="destination" />
|
||||
<DateField source="failure_ts" showTime options={date_format} />
|
||||
<DateField source="retry_last_ts" showTime options={date_format} />
|
||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||
<TextField source="retry_interval" />
|
||||
<TextField source="last_successful_stream_ordering" />
|
||||
</Tab>
|
||||
@@ -149,13 +147,13 @@ export const DestinationShow = props => {
|
||||
<ReferenceManyField
|
||||
reference="destination_rooms"
|
||||
target="destination"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
pagination={<DestinationPagination />}
|
||||
perPage={50}
|
||||
>
|
||||
<Datagrid
|
||||
style={{ width: "100%" }}
|
||||
rowClick={(id, resource, record) => `/rooms/${id}/show`}
|
||||
rowClick={id => `/rooms/${id}/show`}
|
||||
>
|
||||
<TextField
|
||||
source="room_id"
|
||||
@@ -179,7 +177,7 @@ export const DestinationShow = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "destinations",
|
||||
icon: DestinationsIcon,
|
||||
list: DestinationList,
|
@@ -1,51 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
DeleteButton,
|
||||
useDelete,
|
||||
useNotify,
|
||||
useRecordContext,
|
||||
useRefresh,
|
||||
} from "react-admin";
|
||||
|
||||
export const DeviceRemoveButton = props => {
|
||||
const record = useRecordContext();
|
||||
const refresh = useRefresh();
|
||||
const notify = useNotify();
|
||||
|
||||
const [removeDevice] = useDelete();
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
const handleConfirm = () => {
|
||||
removeDevice(
|
||||
"devices",
|
||||
// needs previousData for user_id
|
||||
{ id: record.id, previousData: record },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.devices.action.erase.success");
|
||||
refresh();
|
||||
},
|
||||
onError: () => {
|
||||
notify("resources.devices.action.erase.failure", { type: "error" });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteButton
|
||||
{...props}
|
||||
label="ra.action.remove"
|
||||
confirmTitle="resources.devices.action.erase.title"
|
||||
confirmContent="resources.devices.action.erase.content"
|
||||
onConfirm={handleConfirm}
|
||||
mutationMode="pessimistic"
|
||||
redirect={false}
|
||||
translateOptions={{
|
||||
id: record.id,
|
||||
name: record.display_name ? record.display_name : record.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
25
src/components/devices.tsx
Normal file
25
src/components/devices.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
DeleteWithConfirmButton,
|
||||
DeleteWithConfirmButtonProps,
|
||||
useRecordContext,
|
||||
} from "react-admin";
|
||||
|
||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||
const record = useRecordContext();
|
||||
if (!record) return null;
|
||||
|
||||
return (
|
||||
<DeleteWithConfirmButton
|
||||
{...props}
|
||||
label="ra.action.remove"
|
||||
confirmTitle="resources.devices.action.erase.title"
|
||||
confirmContent="resources.devices.action.erase.content"
|
||||
mutationMode="pessimistic"
|
||||
redirect={false}
|
||||
translateOptions={{
|
||||
id: record.id,
|
||||
name: record.display_name ? record.display_name : record.id,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -1,13 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import get from "lodash/get";
|
||||
import { useState } from "react";
|
||||
import { get } from "lodash";
|
||||
import {
|
||||
BooleanInput,
|
||||
Button,
|
||||
ButtonProps,
|
||||
DateTimeInput,
|
||||
NumberInput,
|
||||
SaveButton,
|
||||
SimpleForm,
|
||||
Toolbar,
|
||||
ToolbarProps,
|
||||
useCreate,
|
||||
useDelete,
|
||||
useNotify,
|
||||
@@ -34,7 +36,7 @@ import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { getMediaUrl } from "../synapse/synapse";
|
||||
|
||||
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
const dateParser = v => {
|
||||
@@ -43,7 +45,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
return d.getTime();
|
||||
};
|
||||
|
||||
const DeleteMediaToolbar = props => (
|
||||
const DeleteMediaToolbar = (props: ToolbarProps) => (
|
||||
<Toolbar {...props}>
|
||||
<SaveButton
|
||||
label="resources.delete_media.action.send"
|
||||
@@ -56,7 +58,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{translate("resources.delete_media.action.send")}
|
||||
</DialogTitle>
|
||||
@@ -92,7 +94,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DeleteMediaButton = props => {
|
||||
export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const notify = useNotify();
|
||||
@@ -101,7 +103,11 @@ export const DeleteMediaButton = props => {
|
||||
const openDialog = () => setOpen(true);
|
||||
const closeDialog = () => setOpen(false);
|
||||
|
||||
const deleteMedia = values => {
|
||||
const deleteMedia = (values: {
|
||||
before_ts: string;
|
||||
size_gt: number;
|
||||
keep_profiles: boolean;
|
||||
}) => {
|
||||
deleteOne(
|
||||
"delete_media",
|
||||
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
|
||||
@@ -148,7 +154,7 @@ export const DeleteMediaButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ProtectMediaButton = () => {
|
||||
export const ProtectMediaButton = (props: ButtonProps) => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
const refresh = useRefresh();
|
||||
@@ -209,7 +215,7 @@ export const ProtectMediaButton = () => {
|
||||
Button instead BooleanField for
|
||||
consistent appearance and position in the column
|
||||
*/}
|
||||
<Button disabled={true}>
|
||||
<Button {...props} disabled={true}>
|
||||
<ClearIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -223,7 +229,7 @@ export const ProtectMediaButton = () => {
|
||||
arrow
|
||||
>
|
||||
<div>
|
||||
<Button onClick={handleUnprotect} disabled={isLoading}>
|
||||
<Button {...props} onClick={handleUnprotect} disabled={isLoading}>
|
||||
<LockIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -236,7 +242,7 @@ export const ProtectMediaButton = () => {
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Button onClick={handleProtect} disabled={isLoading}>
|
||||
<Button {...props} onClick={handleProtect} disabled={isLoading}>
|
||||
<LockOpenIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -246,7 +252,7 @@ export const ProtectMediaButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const QuarantineMediaButton = props => {
|
||||
export const QuarantineMediaButton = (props: ButtonProps) => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
const refresh = useRefresh();
|
||||
@@ -329,7 +335,7 @@ export const QuarantineMediaButton = props => {
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Button onClick={handleQuarantaine} disabled={isLoading}>
|
||||
<Button {...props} onClick={handleQuarantaine} disabled={isLoading}>
|
||||
<BlockIcon />
|
||||
</Button>
|
||||
</div>
|
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BooleanField,
|
||||
BulkDeleteButton,
|
||||
@@ -9,14 +8,17 @@ import {
|
||||
ExportButton,
|
||||
FunctionField,
|
||||
List,
|
||||
ListProps,
|
||||
NumberField,
|
||||
Pagination,
|
||||
ReferenceField,
|
||||
ReferenceManyField,
|
||||
ResourceProps,
|
||||
SearchInput,
|
||||
SelectColumnsButton,
|
||||
SelectField,
|
||||
Show,
|
||||
ShowProps,
|
||||
Tab,
|
||||
TabbedShowLayout,
|
||||
TextField,
|
||||
@@ -58,7 +60,7 @@ const RoomPagination = () => (
|
||||
const RoomTitle = () => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
var name = "";
|
||||
let name = "";
|
||||
if (record) {
|
||||
name = record.name !== "" ? record.name : record.id;
|
||||
}
|
||||
@@ -72,15 +74,15 @@ const RoomTitle = () => {
|
||||
|
||||
const RoomShowActions = () => {
|
||||
const record = useRecordContext();
|
||||
var roomDirectoryStatus = "";
|
||||
if (record) {
|
||||
roomDirectoryStatus = record.public;
|
||||
}
|
||||
|
||||
const publishButton = record.public ? (
|
||||
<RoomDirectoryUnpublishButton />
|
||||
) : (
|
||||
<RoomDirectoryPublishButton />
|
||||
);
|
||||
// FIXME: refresh after (un)publish
|
||||
return (
|
||||
<TopToolbar>
|
||||
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
|
||||
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
|
||||
{publishButton}
|
||||
<DeleteButton
|
||||
mutationMode="pessimistic"
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
@@ -90,7 +92,7 @@ const RoomShowActions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RoomShow = props => {
|
||||
export const RoomShow = (props: ShowProps) => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||
@@ -129,11 +131,11 @@ export const RoomShow = props => {
|
||||
<ReferenceManyField
|
||||
reference="room_members"
|
||||
target="room_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid
|
||||
style={{ width: "100%" }}
|
||||
rowClick={(id, resource, record) => "/users/" + id}
|
||||
rowClick={id => "/users/" + id}
|
||||
bulkActionButtons={false}
|
||||
>
|
||||
<TextField
|
||||
@@ -217,7 +219,7 @@ export const RoomShow = props => {
|
||||
<ReferenceManyField
|
||||
reference="room_state"
|
||||
target="room_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||
<TextField source="type" sortable={false} />
|
||||
@@ -255,7 +257,7 @@ export const RoomShow = props => {
|
||||
<ReferenceManyField
|
||||
reference="forward_extremities"
|
||||
target="room_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||
<TextField source="id" sortable={false} />
|
||||
@@ -296,7 +298,7 @@ const RoomListActions = () => (
|
||||
</TopToolbar>
|
||||
);
|
||||
|
||||
export const RoomList = props => {
|
||||
export const RoomList = (props: ListProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -345,7 +347,7 @@ export const RoomList = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "rooms",
|
||||
icon: RoomIcon,
|
||||
list: RoomList,
|
@@ -1,42 +1,26 @@
|
||||
import React from "react";
|
||||
import { cloneElement } from "react";
|
||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||
import {
|
||||
Datagrid,
|
||||
ExportButton,
|
||||
List,
|
||||
ListProps,
|
||||
NumberField,
|
||||
Pagination,
|
||||
sanitizeListRestProps,
|
||||
ResourceProps,
|
||||
SearchInput,
|
||||
TextField,
|
||||
TopToolbar,
|
||||
useListContext,
|
||||
} from "react-admin";
|
||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||
|
||||
import { DeleteMediaButton } from "./media";
|
||||
|
||||
const ListActions = props => {
|
||||
const { className, exporter, filters, maxResults, ...rest } = props;
|
||||
const { sort, resource, displayedFilters, filterValues, showFilter, total } =
|
||||
useListContext();
|
||||
const ListActions = () => {
|
||||
const { isLoading, total } = useListContext();
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
{filters &&
|
||||
cloneElement(filters, {
|
||||
resource,
|
||||
showFilter,
|
||||
displayedFilters,
|
||||
filterValues,
|
||||
context: "button",
|
||||
})}
|
||||
<TopToolbar>
|
||||
<DeleteMediaButton />
|
||||
<ExportButton
|
||||
disabled={total === 0}
|
||||
resource={resource}
|
||||
sort={sort}
|
||||
filterValues={filterValues}
|
||||
maxResults={maxResults}
|
||||
/>
|
||||
<ExportButton disabled={isLoading || total === 0} />
|
||||
</TopToolbar>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +31,7 @@ const UserMediaStatsPagination = () => (
|
||||
|
||||
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
|
||||
|
||||
export const UserMediaStatsList = props => (
|
||||
export const UserMediaStatsList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
actions={<ListActions />}
|
||||
@@ -56,7 +40,7 @@ export const UserMediaStatsList = props => (
|
||||
sort={{ field: "media_length", order: "DESC" }}
|
||||
>
|
||||
<Datagrid
|
||||
rowClick={(id, resource, record) => "/users/" + id + "/media"}
|
||||
rowClick={id => "/users/" + id + "/media"}
|
||||
bulkActionButtons={false}
|
||||
>
|
||||
<TextField source="user_id" label="resources.users.fields.id" />
|
||||
@@ -70,7 +54,7 @@ export const UserMediaStatsList = props => (
|
||||
</List>
|
||||
);
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "user_media_statistics",
|
||||
icon: EqualizerIcon,
|
||||
list: UserMediaStatsList,
|
@@ -1,4 +1,3 @@
|
||||
import React, { cloneElement } from "react";
|
||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||
import ContactMailIcon from "@mui/icons-material/ContactMail";
|
||||
import DevicesIcon from "@mui/icons-material/Devices";
|
||||
@@ -16,9 +15,11 @@ import {
|
||||
Datagrid,
|
||||
DateField,
|
||||
Create,
|
||||
CreateProps,
|
||||
Edit,
|
||||
EditProps,
|
||||
List,
|
||||
Toolbar,
|
||||
ListProps,
|
||||
SimpleForm,
|
||||
SimpleFormIterator,
|
||||
TabbedForm,
|
||||
@@ -30,11 +31,11 @@ import {
|
||||
TextInput,
|
||||
ReferenceField,
|
||||
ReferenceManyField,
|
||||
ResourceProps,
|
||||
SearchInput,
|
||||
SelectInput,
|
||||
BulkDeleteButton,
|
||||
DeleteButton,
|
||||
SaveButton,
|
||||
maxLength,
|
||||
regex,
|
||||
required,
|
||||
@@ -44,8 +45,8 @@ import {
|
||||
CreateButton,
|
||||
ExportButton,
|
||||
TopToolbar,
|
||||
sanitizeListRestProps,
|
||||
NumberField,
|
||||
useListContext,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
import AvatarField from "./AvatarField";
|
||||
@@ -67,7 +68,7 @@ const choices_type = [
|
||||
{ id: "support", name: "support" },
|
||||
];
|
||||
|
||||
const date_format = {
|
||||
const date_format: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
@@ -76,43 +77,12 @@ const date_format = {
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const UserListActions = ({
|
||||
sort,
|
||||
className,
|
||||
resource,
|
||||
filters,
|
||||
displayedFilters,
|
||||
exporter, // you can hide ExportButton if exporter = (null || false)
|
||||
filterValues,
|
||||
permanentFilter,
|
||||
hasCreate, // you can hide CreateButton if hasCreate = false
|
||||
selectedIds,
|
||||
onUnselectItems,
|
||||
showFilter,
|
||||
maxResults,
|
||||
total,
|
||||
...rest
|
||||
}) => {
|
||||
const UserListActions = () => {
|
||||
const { isLoading, total } = useListContext();
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
{filters &&
|
||||
cloneElement(filters, {
|
||||
resource,
|
||||
showFilter,
|
||||
displayedFilters,
|
||||
filterValues,
|
||||
context: "button",
|
||||
})}
|
||||
<TopToolbar>
|
||||
<CreateButton />
|
||||
<ExportButton
|
||||
disabled={total === 0}
|
||||
resource={resource}
|
||||
sort={sort}
|
||||
filter={{ ...filterValues, ...permanentFilter }}
|
||||
exporter={exporter}
|
||||
maxResults={maxResults}
|
||||
/>
|
||||
{/* Add your custom actions */}
|
||||
<ExportButton disabled={isLoading || total === 0} maxResults={10000} />
|
||||
<Button component={Link} to="/import_users" label="CSV Import">
|
||||
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
|
||||
</Button>
|
||||
@@ -150,13 +120,13 @@ const UserBulkActionButtons = () => (
|
||||
</>
|
||||
);
|
||||
|
||||
export const UserList = props => (
|
||||
export const UserList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
filters={userFilters}
|
||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||
sort={{ field: "name", order: "ASC" }}
|
||||
actions={<UserListActions maxResults={10000} />}
|
||||
actions={<UserListActions />}
|
||||
pagination={<UserPagination />}
|
||||
>
|
||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
||||
@@ -233,24 +203,14 @@ export function generateRandomUser() {
|
||||
};
|
||||
}
|
||||
|
||||
const UserEditToolbar = props => (
|
||||
<Toolbar {...props}>
|
||||
<SaveButton disabled={props.pristine} />
|
||||
</Toolbar>
|
||||
);
|
||||
|
||||
const UserEditActions = ({ data }) => {
|
||||
const UserEditActions = () => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
var userStatus = "";
|
||||
if (data) {
|
||||
userStatus = data.deactivated;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopToolbar>
|
||||
{!userStatus && <ServerNoticeButton record={data} />}
|
||||
{!record.deactivated && <ServerNoticeButton />}
|
||||
<DeleteButton
|
||||
record={data}
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle={translate("resources.users.helper.erase", {
|
||||
smart_count: 1,
|
||||
@@ -261,7 +221,7 @@ const UserEditActions = ({ data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const UserCreate = props => (
|
||||
export const UserCreate = (props: CreateProps) => (
|
||||
<Create {...props}>
|
||||
<SimpleForm>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
@@ -315,11 +275,11 @@ const UserTitle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const UserEdit = props => {
|
||||
export const UserEdit = (props: EditProps) => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<TabbedForm>
|
||||
<FormTab
|
||||
label={translate("resources.users.name", { smart_count: 1 })}
|
||||
icon={<PersonPinIcon />}
|
||||
@@ -389,7 +349,7 @@ export const UserEdit = props => {
|
||||
<ReferenceManyField
|
||||
reference="devices"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<TextField source="device_id" sortable={false} />
|
||||
@@ -414,7 +374,7 @@ export const UserEdit = props => {
|
||||
<ReferenceField
|
||||
reference="connections"
|
||||
source="id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
link={false}
|
||||
>
|
||||
<ArrayField
|
||||
@@ -447,7 +407,7 @@ export const UserEdit = props => {
|
||||
<ReferenceManyField
|
||||
reference="users_media"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
pagination={<UserPagination />}
|
||||
perPage={50}
|
||||
sort={{ field: "created_ts", order: "DESC" }}
|
||||
@@ -479,11 +439,11 @@ export const UserEdit = props => {
|
||||
<ReferenceManyField
|
||||
reference="joined_rooms"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid
|
||||
style={{ width: "100%" }}
|
||||
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
|
||||
rowClick={id => "/rooms/" + id + "/show"}
|
||||
bulkActionButtons={false}
|
||||
>
|
||||
<TextField
|
||||
@@ -512,7 +472,7 @@ export const UserEdit = props => {
|
||||
<ReferenceManyField
|
||||
reference="pushers"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
label={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||
<TextField source="kind" sortable={false} />
|
||||
@@ -531,7 +491,7 @@ export const UserEdit = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const resource = {
|
||||
const resource: ResourceProps = {
|
||||
name: "users",
|
||||
icon: UserIcon,
|
||||
list: UserList,
|
@@ -1,6 +1,7 @@
|
||||
import { formalGermanMessages } from "@haleos/ra-language-german";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const de = {
|
||||
const de: SynapseTranslationMessages = {
|
||||
...formalGermanMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
@@ -1,6 +1,7 @@
|
||||
import englishMessages from "ra-language-english";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const en = {
|
||||
const en: SynapseTranslationMessages = {
|
||||
...englishMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
@@ -18,6 +19,7 @@ const en = {
|
||||
tabs: { sso: "SSO" },
|
||||
},
|
||||
rooms: {
|
||||
details: "Room details",
|
||||
tabs: {
|
||||
basic: "Basic",
|
||||
members: "Members",
|
@@ -1,6 +1,7 @@
|
||||
import farsiMessages from "ra-language-farsi";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const fa = {
|
||||
const fa: SynapseTranslationMessages = {
|
||||
...farsiMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
@@ -1,6 +1,7 @@
|
||||
import frenchMessages from "ra-language-french";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const fr = {
|
||||
const fr: SynapseTranslationMessages = {
|
||||
...frenchMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
90
src/i18n/index.d.ts
vendored
Normal file
90
src/i18n/index.d.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import { TranslationMessages } from "ra-core";
|
||||
|
||||
interface SynapseTranslationMessages extends TranslationMessages {
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: string;
|
||||
welcome: string;
|
||||
server_version: string;
|
||||
supports_specs?: string; // TODO: fa, fr, it, zh
|
||||
username_error: string;
|
||||
protocol_error: string;
|
||||
url_error: string;
|
||||
sso_sign_in: string;
|
||||
};
|
||||
users: {
|
||||
invalid_user_id: string;
|
||||
tabs: { sso: string };
|
||||
};
|
||||
rooms: {
|
||||
details?: string; // TODO: fa, fr, it, zh
|
||||
tabs: {
|
||||
basic: string;
|
||||
members: string;
|
||||
detail: string;
|
||||
permission: string;
|
||||
};
|
||||
};
|
||||
reports: { tabs: { basic: string; detail: string } };
|
||||
};
|
||||
import_users: {
|
||||
error: {
|
||||
at_entry: string;
|
||||
error: string;
|
||||
required_field: string;
|
||||
invalid_value: string;
|
||||
unreasonably_big: string;
|
||||
already_in_progress: string;
|
||||
id_exits: string;
|
||||
};
|
||||
title: string;
|
||||
goToPdf: string;
|
||||
cards: {
|
||||
importstats: {
|
||||
header: string;
|
||||
users_total: string;
|
||||
guest_count: string;
|
||||
admin_count: string;
|
||||
};
|
||||
conflicts: {
|
||||
header: string;
|
||||
mode: {
|
||||
stop: string;
|
||||
skip: string;
|
||||
};
|
||||
};
|
||||
ids: {
|
||||
header: string;
|
||||
all_ids_present: string;
|
||||
count_ids_present: string;
|
||||
mode: {
|
||||
ignore: string;
|
||||
update: string;
|
||||
};
|
||||
};
|
||||
passwords: {
|
||||
header: string;
|
||||
all_passwords_present: string;
|
||||
count_passwords_present: string;
|
||||
use_passwords: string;
|
||||
};
|
||||
upload: {
|
||||
header: string;
|
||||
explanation: string;
|
||||
};
|
||||
startImport: {
|
||||
simulate_only: string;
|
||||
run_import: string;
|
||||
};
|
||||
results: {
|
||||
header: string;
|
||||
total: string;
|
||||
successful: string;
|
||||
skipped: string;
|
||||
download_skipped: string;
|
||||
with_error: string;
|
||||
simulated_only: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import italianMessages from "ra-language-italian";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const it = {
|
||||
const it: SynapseTranslationMessages = {
|
||||
...italianMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
@@ -1,6 +1,7 @@
|
||||
import chineseMessages from "@haxqer/ra-language-chinese";
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const zh = {
|
||||
const zh: SynapseTranslationMessages = {
|
||||
...chineseMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
@@ -24,11 +25,6 @@ const zh = {
|
||||
detail: "细节",
|
||||
permission: "权限",
|
||||
},
|
||||
delete: {
|
||||
title: "删除房间",
|
||||
message:
|
||||
"您确定要删除这个房间吗?该操作无法被撤销。这个房间里所有的消息和分享的媒体都将被从服务器上删除!",
|
||||
},
|
||||
},
|
||||
reports: { tabs: { basic: "基本", detail: "细节" } },
|
||||
},
|
1
src/jest.setup.ts
Normal file
1
src/jest.setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
@@ -1,3 +0,0 @@
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
|
||||
fetchMock.enableMocks();
|
@@ -1,14 +1,17 @@
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
import authProvider from "./authProvider";
|
||||
|
||||
fetchMock.enableMocks();
|
||||
|
||||
describe("authProvider", () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
fetchMock.resetMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("login", () => {
|
||||
it("should successfully login with username and password", async () => {
|
||||
fetch.once(
|
||||
fetchMock.once(
|
||||
JSON.stringify({
|
||||
home_server: "example.com",
|
||||
user_id: "@user:example.com",
|
||||
@@ -17,7 +20,7 @@ describe("authProvider", () => {
|
||||
})
|
||||
);
|
||||
|
||||
const ret = await authProvider.login({
|
||||
const ret: undefined = await authProvider.login({
|
||||
base_url: "http://example.com",
|
||||
username: "@user:example.com",
|
||||
password: "secret",
|
||||
@@ -29,8 +32,8 @@ describe("authProvider", () => {
|
||||
{
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
"Content-Type": ["application/json"],
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
method: "POST",
|
||||
}
|
||||
@@ -43,7 +46,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
it("should successfully login with token", async () => {
|
||||
fetch.once(
|
||||
fetchMock.once(
|
||||
JSON.stringify({
|
||||
home_server: "example.com",
|
||||
user_id: "@user:example.com",
|
||||
@@ -52,19 +55,19 @@ describe("authProvider", () => {
|
||||
})
|
||||
);
|
||||
|
||||
const ret = await authProvider.login({
|
||||
const ret: undefined = await authProvider.login({
|
||||
base_url: "https://example.com/",
|
||||
loginToken: "login_token",
|
||||
});
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(fetch).toBeCalledWith(
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://example.com/_matrix/client/r0/login",
|
||||
{
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
"Content-Type": ["application/json"],
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
method: "POST",
|
||||
}
|
||||
@@ -79,14 +82,14 @@ describe("authProvider", () => {
|
||||
it("should remove the access_token from localStorage", async () => {
|
||||
localStorage.setItem("base_url", "example.com");
|
||||
localStorage.setItem("access_token", "foo");
|
||||
fetch.mockResponse(JSON.stringify({}));
|
||||
fetchMock.mockResponse(JSON.stringify({}));
|
||||
|
||||
await authProvider.logout();
|
||||
await authProvider.logout(null);
|
||||
|
||||
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
Authorization: ["Bearer foo"],
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer foo",
|
||||
}),
|
||||
method: "POST",
|
||||
user: { authenticated: true, token: "Bearer foo" },
|
||||
@@ -129,7 +132,7 @@ describe("authProvider", () => {
|
||||
|
||||
describe("getPermissions", () => {
|
||||
it("should do nothing", async () => {
|
||||
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
|
||||
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,10 +1,20 @@
|
||||
import { fetchUtils } from "react-admin";
|
||||
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
||||
|
||||
const authProvider = {
|
||||
const authProvider: AuthProvider = {
|
||||
// called when the user attempts to log in
|
||||
login: async ({ base_url, username, password, loginToken }) => {
|
||||
login: async ({
|
||||
base_url,
|
||||
username,
|
||||
password,
|
||||
loginToken,
|
||||
}: {
|
||||
base_url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
loginToken: string;
|
||||
}) => {
|
||||
console.log("login ");
|
||||
const options = {
|
||||
const options: Options = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(
|
||||
Object.assign(
|
||||
@@ -49,7 +59,7 @@ const authProvider = {
|
||||
localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
|
||||
const access_token = localStorage.getItem("access_token");
|
||||
|
||||
const options = {
|
||||
const options: Options = {
|
||||
method: "POST",
|
||||
user: {
|
||||
authenticated: true,
|
||||
@@ -63,7 +73,7 @@ const authProvider = {
|
||||
}
|
||||
},
|
||||
// called when the API returns an error
|
||||
checkError: ({ status }) => {
|
||||
checkError: ({ status }: { status: number }) => {
|
||||
console.log("checkError " + status);
|
||||
if (status === 401 || status === 403) {
|
||||
return Promise.reject();
|
@@ -1,7 +1,10 @@
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
import dataProvider from "./dataProvider";
|
||||
|
||||
fetchMock.enableMocks();
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
|
||||
describe("dataProvider", () => {
|
||||
@@ -9,7 +12,7 @@ describe("dataProvider", () => {
|
||||
localStorage.setItem("access_token", "access_token");
|
||||
|
||||
it("fetches all users", async () => {
|
||||
fetch.mockResponseOnce(
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
users: [
|
||||
{
|
||||
@@ -48,7 +51,7 @@ describe("dataProvider", () => {
|
||||
});
|
||||
|
||||
it("fetches one user", async () => {
|
||||
fetch.mockResponseOnce(
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
name: "user_id1",
|
||||
password: "user_password",
|
@@ -1,8 +1,15 @@
|
||||
import { fetchUtils } from "react-admin";
|
||||
import {
|
||||
DataProvider,
|
||||
DeleteParams,
|
||||
Identifier,
|
||||
Options,
|
||||
RaRecord,
|
||||
fetchUtils,
|
||||
} from "react-admin";
|
||||
import { stringify } from "query-string";
|
||||
|
||||
// Adds the access token to all requests
|
||||
const jsonClient = (url, options = {}) => {
|
||||
const jsonClient = (url: string, options: Options = {}) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
console.log("httpClient " + url);
|
||||
if (token != null) {
|
||||
@@ -14,10 +21,10 @@ const jsonClient = (url, options = {}) => {
|
||||
return fetchUtils.fetchJson(url, options);
|
||||
};
|
||||
|
||||
const mxcUrlToHttp = mxcUrl => {
|
||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||
var ret = re.exec(mxcUrl);
|
||||
const ret = re.exec(mxcUrl);
|
||||
console.log("mxcClient " + ret);
|
||||
if (ret == null) return null;
|
||||
const serverName = ret[1];
|
||||
@@ -25,13 +32,188 @@ const mxcUrlToHttp = mxcUrl => {
|
||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
||||
};
|
||||
|
||||
interface Room {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
canonical_alias?: string;
|
||||
avatar_url?: string;
|
||||
joined_members: number;
|
||||
joined_local_members: number;
|
||||
version: number;
|
||||
creator: string;
|
||||
encryption?: string;
|
||||
federatable: boolean;
|
||||
public: boolean;
|
||||
join_rules: "public" | "knock" | "invite" | "private";
|
||||
guest_access?: "can_join" | "forbidden";
|
||||
history_visibility: "invited" | "joined" | "shared" | "world_readable";
|
||||
state_events: number;
|
||||
room_type?: string;
|
||||
}
|
||||
|
||||
interface RoomState {
|
||||
age: number;
|
||||
content: {
|
||||
alias?: string;
|
||||
};
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
room_id: string;
|
||||
sender: string;
|
||||
state_key: string;
|
||||
type: string;
|
||||
user_id: string;
|
||||
unsigned: {
|
||||
age?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ForwardExtremity {
|
||||
event_id: string;
|
||||
state_group: number;
|
||||
depth: number;
|
||||
received_ts: number;
|
||||
}
|
||||
|
||||
interface EventReport {
|
||||
id: number;
|
||||
received_ts: number;
|
||||
room_id: string;
|
||||
name: string;
|
||||
event_id: string;
|
||||
user_id: string;
|
||||
reason?: string;
|
||||
score?: number;
|
||||
sender: string;
|
||||
canonical_alias?: string;
|
||||
}
|
||||
|
||||
interface Threepid {
|
||||
medium: string;
|
||||
address: string;
|
||||
added_at: number;
|
||||
validated_at: number;
|
||||
}
|
||||
|
||||
interface ExternalId {
|
||||
auth_provider: string;
|
||||
external_id: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
displayname?: string;
|
||||
threepids: Threepid[];
|
||||
avatar_url?: string;
|
||||
is_guest: 0 | 1;
|
||||
admin: 0 | 1;
|
||||
deactivated: 0 | 1;
|
||||
erased: boolean;
|
||||
shadow_banned: 0 | 1;
|
||||
creation_ts: number;
|
||||
appservice_id?: string;
|
||||
consent_server_notice_sent?: string;
|
||||
consent_version?: string;
|
||||
consent_ts?: number;
|
||||
external_ids: ExternalId[];
|
||||
user_type?: string;
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
device_id: string;
|
||||
display_name?: string;
|
||||
last_seen_ip?: string;
|
||||
last_seen_user_agent?: string;
|
||||
last_seen_ts?: number;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
ip: string;
|
||||
last_seen: number;
|
||||
user_agent: string;
|
||||
}
|
||||
|
||||
interface Whois {
|
||||
user_id: string;
|
||||
devices: Record<
|
||||
string,
|
||||
{
|
||||
sessions: {
|
||||
connections: Connection[];
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface Pusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
url?: string;
|
||||
format: string;
|
||||
};
|
||||
url: string;
|
||||
format: string;
|
||||
device_display_name: string;
|
||||
profile_tag: string;
|
||||
kind: string;
|
||||
lang: string;
|
||||
pushkey: string;
|
||||
}
|
||||
|
||||
interface UserMedia {
|
||||
created_ts: number;
|
||||
last_access_ts?: number;
|
||||
media_id: string;
|
||||
media_length: number;
|
||||
media_type: string;
|
||||
quarantined_by?: string;
|
||||
safe_from_quarantine: boolean;
|
||||
upload_name?: string;
|
||||
}
|
||||
|
||||
interface UserMediaStatistic {
|
||||
displayname: string;
|
||||
media_count: number;
|
||||
media_length: number;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
interface RegistrationToken {
|
||||
token: string;
|
||||
uses_allowed: number;
|
||||
pending: number;
|
||||
completed: number;
|
||||
expiry_time?: number;
|
||||
}
|
||||
|
||||
interface RaServerNotice {
|
||||
id: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface Destination {
|
||||
destination: string;
|
||||
retry_last_ts: number;
|
||||
retry_interval: number;
|
||||
failure_ts: number;
|
||||
last_successful_stream_ordering?: number;
|
||||
}
|
||||
|
||||
interface DestinationRoom {
|
||||
room_id: string;
|
||||
stream_ordering: number;
|
||||
}
|
||||
|
||||
const resourceMap = {
|
||||
users: {
|
||||
path: "/_synapse/admin/v2/users",
|
||||
map: u => ({
|
||||
map: (u: User) => ({
|
||||
...u,
|
||||
id: u.name,
|
||||
avatar_src: mxcUrlToHttp(u.avatar_url),
|
||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
||||
is_guest: !!u.is_guest,
|
||||
admin: !!u.admin,
|
||||
deactivated: !!u.deactivated,
|
||||
@@ -40,14 +222,14 @@ const resourceMap = {
|
||||
}),
|
||||
data: "users",
|
||||
total: json => json.total,
|
||||
create: data => ({
|
||||
create: (data: RaRecord) => ({
|
||||
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
|
||||
data.id
|
||||
)}:${localStorage.getItem("home_server")}`,
|
||||
body: data,
|
||||
method: "PUT",
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
|
||||
params.id
|
||||
)}`,
|
||||
@@ -57,7 +239,7 @@ const resourceMap = {
|
||||
},
|
||||
rooms: {
|
||||
path: "/_synapse/admin/v1/rooms",
|
||||
map: r => ({
|
||||
map: (r: Room) => ({
|
||||
...r,
|
||||
id: r.room_id,
|
||||
alias: r.canonical_alias,
|
||||
@@ -67,36 +249,29 @@ const resourceMap = {
|
||||
public: !!r.public,
|
||||
}),
|
||||
data: "rooms",
|
||||
total: json => {
|
||||
return json.total_rooms;
|
||||
},
|
||||
delete: params => ({
|
||||
total: json => json.total_rooms,
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
||||
body: { block: false },
|
||||
}),
|
||||
},
|
||||
reports: {
|
||||
path: "/_synapse/admin/v1/event_reports",
|
||||
map: er => ({
|
||||
...er,
|
||||
id: er.id,
|
||||
}),
|
||||
map: (er: EventReport) => ({ ...er }),
|
||||
data: "event_reports",
|
||||
total: json => json.total,
|
||||
},
|
||||
devices: {
|
||||
map: d => ({
|
||||
map: (d: Device) => ({
|
||||
...d,
|
||||
id: d.device_id,
|
||||
}),
|
||||
data: "devices",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
reference: id => ({
|
||||
total: json => json.total,
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
|
||||
params.previousData.user_id
|
||||
)}/devices/${params.id}`,
|
||||
@@ -104,84 +279,74 @@ const resourceMap = {
|
||||
},
|
||||
connections: {
|
||||
path: "/_synapse/admin/v1/whois",
|
||||
map: c => ({
|
||||
map: (c: Whois) => ({
|
||||
...c,
|
||||
id: c.user_id,
|
||||
}),
|
||||
data: "connections",
|
||||
},
|
||||
room_members: {
|
||||
map: m => ({
|
||||
map: (m: string) => ({
|
||||
id: m,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
|
||||
}),
|
||||
data: "members",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
},
|
||||
room_state: {
|
||||
map: rs => ({
|
||||
map: (rs: RoomState) => ({
|
||||
...rs,
|
||||
id: rs.event_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
|
||||
}),
|
||||
data: "state",
|
||||
total: json => {
|
||||
return json.state.length;
|
||||
},
|
||||
total: json => json.state.length,
|
||||
},
|
||||
pushers: {
|
||||
map: p => ({
|
||||
map: (p: Pusher) => ({
|
||||
...p,
|
||||
id: p.pushkey,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
|
||||
}),
|
||||
data: "pushers",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
},
|
||||
joined_rooms: {
|
||||
map: jr => ({
|
||||
map: (jr: string) => ({
|
||||
id: jr,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
|
||||
id
|
||||
)}/joined_rooms`,
|
||||
}),
|
||||
data: "joined_rooms",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
},
|
||||
users_media: {
|
||||
map: um => ({
|
||||
map: (um: UserMedia) => ({
|
||||
...um,
|
||||
id: um.media_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
||||
}),
|
||||
data: "media",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
delete: params => ({
|
||||
total: json => json.total,
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
||||
"home_server"
|
||||
)}/${params.id}`,
|
||||
}),
|
||||
},
|
||||
delete_media: {
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
|
||||
"home_server"
|
||||
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
|
||||
@@ -191,25 +356,25 @@ const resourceMap = {
|
||||
}),
|
||||
},
|
||||
protect_media: {
|
||||
map: pm => ({ id: pm.media_id }),
|
||||
create: params => ({
|
||||
map: (pm: UserMedia) => ({ id: pm.media_id }),
|
||||
create: (params: UserMedia) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
|
||||
method: "POST",
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
|
||||
method: "POST",
|
||||
}),
|
||||
},
|
||||
quarantine_media: {
|
||||
map: qm => ({ id: qm.media_id }),
|
||||
create: params => ({
|
||||
map: (qm: UserMedia) => ({ id: qm.media_id }),
|
||||
create: (params: UserMedia) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
|
||||
"home_server"
|
||||
)}/${params.media_id}`,
|
||||
method: "POST",
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
|
||||
"home_server"
|
||||
)}/${params.id}`,
|
||||
@@ -217,8 +382,8 @@ const resourceMap = {
|
||||
}),
|
||||
},
|
||||
servernotices: {
|
||||
map: n => ({ id: n.event_id }),
|
||||
create: data => ({
|
||||
map: (n: { event_id: string }) => ({ id: n.event_id }),
|
||||
create: (data: RaServerNotice) => ({
|
||||
endpoint: "/_synapse/admin/v1/send_server_notice",
|
||||
body: {
|
||||
user_id: data.id,
|
||||
@@ -232,50 +397,44 @@ const resourceMap = {
|
||||
},
|
||||
user_media_statistics: {
|
||||
path: "/_synapse/admin/v1/statistics/users/media",
|
||||
map: usms => ({
|
||||
map: (usms: UserMediaStatistic) => ({
|
||||
...usms,
|
||||
id: usms.user_id,
|
||||
}),
|
||||
data: "users",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
},
|
||||
forward_extremities: {
|
||||
map: fe => ({
|
||||
map: (fe: ForwardExtremity) => ({
|
||||
...fe,
|
||||
id: fe.event_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
|
||||
}),
|
||||
data: "results",
|
||||
total: json => {
|
||||
return json.count;
|
||||
},
|
||||
delete: params => ({
|
||||
total: json => json.count,
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
|
||||
}),
|
||||
},
|
||||
room_directory: {
|
||||
path: "/_matrix/client/r0/publicRooms",
|
||||
map: rd => ({
|
||||
map: (rd: Room) => ({
|
||||
...rd,
|
||||
id: rd.room_id,
|
||||
public: !!rd.public,
|
||||
guest_access: !!rd.guest_access,
|
||||
avatar_src: mxcUrlToHttp(rd.avatar_url),
|
||||
avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
|
||||
}),
|
||||
data: "chunk",
|
||||
total: json => {
|
||||
return json.total_room_count_estimate;
|
||||
},
|
||||
create: params => ({
|
||||
total: json => json.total_room_count_estimate,
|
||||
create: (params: RaRecord) => ({
|
||||
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
||||
body: { visibility: "public" },
|
||||
method: "PUT",
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
|
||||
body: { visibility: "private" },
|
||||
method: "PUT",
|
||||
@@ -283,54 +442,49 @@ const resourceMap = {
|
||||
},
|
||||
destinations: {
|
||||
path: "/_synapse/admin/v1/federation/destinations",
|
||||
map: dst => ({
|
||||
map: (dst: Destination) => ({
|
||||
...dst,
|
||||
id: dst.destination,
|
||||
}),
|
||||
data: "destinations",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
delete: params => ({
|
||||
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
|
||||
method: "POST",
|
||||
}),
|
||||
},
|
||||
destination_rooms: {
|
||||
map: dstroom => ({
|
||||
map: (dstroom: DestinationRoom) => ({
|
||||
...dstroom,
|
||||
id: dstroom.room_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
|
||||
}),
|
||||
data: "rooms",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
total: json => json.total,
|
||||
},
|
||||
registration_tokens: {
|
||||
path: "/_synapse/admin/v1/registration_tokens",
|
||||
map: rt => ({
|
||||
map: (rt: RegistrationToken) => ({
|
||||
...rt,
|
||||
id: rt.token,
|
||||
}),
|
||||
data: "registration_tokens",
|
||||
total: json => {
|
||||
return json.registration_tokens.length;
|
||||
},
|
||||
create: params => ({
|
||||
total: json => json.registration_tokens.length,
|
||||
create: (params: RaRecord) => ({
|
||||
endpoint: "/_synapse/admin/v1/registration_tokens/new",
|
||||
body: params,
|
||||
method: "POST",
|
||||
}),
|
||||
delete: params => ({
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
function filterNullValues(key, value) {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function filterNullValues(key: string, value: any) {
|
||||
// Filtering out null properties
|
||||
// to reset user_type from user, it must be null
|
||||
if (value === null && key !== "user_type") {
|
||||
@@ -339,7 +493,7 @@ function filterNullValues(key, value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function getSearchOrder(order) {
|
||||
function getSearchOrder(order: "ASC" | "DESC") {
|
||||
if (order === "DESC") {
|
||||
return "b";
|
||||
} else {
|
||||
@@ -347,7 +501,7 @@ function getSearchOrder(order) {
|
||||
}
|
||||
}
|
||||
|
||||
const dataProvider = {
|
||||
const dataProvider: DataProvider = {
|
||||
getList: async (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const {
|
||||
@@ -376,7 +530,8 @@ const dataProvider = {
|
||||
dir: getSearchOrder(order),
|
||||
};
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -393,7 +548,8 @@ const dataProvider = {
|
||||
getOne: async (resource, params) => {
|
||||
console.log("getOne " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -407,7 +563,8 @@ const dataProvider = {
|
||||
getMany: async (resource, params) => {
|
||||
console.log("getMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homerserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -436,7 +593,8 @@ const dataProvider = {
|
||||
};
|
||||
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -453,7 +611,8 @@ const dataProvider = {
|
||||
update: async (resource, params) => {
|
||||
console.log("update " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -471,7 +630,8 @@ const dataProvider = {
|
||||
updateMany: async (resource, params) => {
|
||||
console.log("updateMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -491,7 +651,8 @@ const dataProvider = {
|
||||
create: async (resource, params) => {
|
||||
console.log("create " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
if (!("create" in res)) return Promise.reject();
|
||||
@@ -505,13 +666,17 @@ const dataProvider = {
|
||||
return { data: res.map(json) };
|
||||
},
|
||||
|
||||
createMany: async (resource, params) => {
|
||||
createMany: async (
|
||||
resource: string,
|
||||
params: { ids: Identifier[]; data: RaRecord }
|
||||
) => {
|
||||
console.log("createMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
if (!("create" in res)) return Promise.reject();
|
||||
if (!("create" in res)) throw Error(`Create ${resource} is not allowed`);
|
||||
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(id => {
|
||||
@@ -530,7 +695,8 @@ const dataProvider = {
|
||||
delete: async (resource, params) => {
|
||||
console.log("delete " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -555,7 +721,8 @@ const dataProvider = {
|
||||
deleteMany: async (resource, params) => {
|
||||
console.log("deleteMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
if (!homeserver || !(resource in resourceMap))
|
||||
throw Error("Homeserver not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
@@ -579,7 +746,7 @@ const dataProvider = {
|
||||
params.ids.map(id =>
|
||||
jsonClient(`${endpoint_url}/${id}`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params.data, filterNullValues),
|
||||
// body: JSON.stringify(params.data, filterNullValues), @FIXME
|
||||
})
|
||||
)
|
||||
);
|
Reference in New Issue
Block a user