Transform code base to typescript

Change-Id: Ia1f862fb5962ddd54b8d7643abbc39bb314d1f8e
This commit is contained in:
Manuel Stahl
2024-04-22 14:23:55 +02:00
parent 03fcd8126a
commit 2466af6936
45 changed files with 1081 additions and 516 deletions

View File

@@ -1,4 +1,3 @@
import React from "react";
import { RecordContextProvider } from "react-admin";
import { render, screen } from "@testing-library/react";
import AvatarField from "./AvatarField";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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")} />,

View File

@@ -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";

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
View 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}`;
};

View File

@@ -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,

View File

@@ -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,
}}
/>
);
};

View 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,
}}
/>
);
};

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,