Compare commits

2 Commits

Author SHA1 Message Date
Aine
abc922c956 Enable visual customization (#81)
* Enable visual customization

* update readme
2024-10-23 00:03:06 +03:00
Aine
4f2cd38344 Add user profile to the top menu (#80)
* Add user profile to the top menu

* update readme
2024-10-22 12:18:55 +03:00
10 changed files with 73 additions and 19 deletions

View File

@@ -69,6 +69,8 @@ The following changes are already implemented:
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58) * [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
* [Fix footer causing vertical scrollbar](https://github.com/etkecc/synapse-admin/pull/60) * [Fix footer causing vertical scrollbar](https://github.com/etkecc/synapse-admin/pull/60)
* [Custom Menu Items](https://github.com/etkecc/synapse-admin/pull/79) * [Custom Menu Items](https://github.com/etkecc/synapse-admin/pull/79)
* [Add user profile to the top menu](https://github.com/etkecc/synapse-admin/pull/80)
* [Enable visual customization](https://github.com/etkecc/synapse-admin/pull/81)
_the list will be updated as new changes are added_ _the list will be updated as new changes are added_

View File

@@ -1,4 +1,4 @@
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin"; import { AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
import { LoginMethod } from "../pages/LoginPage"; import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react"; import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "./icons"; import { Icons, DefaultIcon } from "./icons";
@@ -44,7 +44,12 @@ const AdminUserMenu = () => {
); );
}; };
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />; const AdminAppBar = () => {
return (<AppBar userMenu={<AdminUserMenu />}>
<TitlePortal />
<InspectorButton />
</AppBar>);
};
const AdminMenu = (props) => { const AdminMenu = (props) => {
const [menu, setMenu] = useState([]); const [menu, setMenu] = useState([]);
@@ -52,7 +57,11 @@ const AdminMenu = (props) => {
useEffect(() => { useEffect(() => {
const menuConfig = localStorage.getItem('menu'); const menuConfig = localStorage.getItem('menu');
if (menuConfig) { if (menuConfig) {
setMenu(JSON.parse(menuConfig)); try {
setMenu(JSON.parse(menuConfig));
} catch (e) {
console.error('Error parsing menu configuration', e);
}
} }
}, []); }, []);

View File

@@ -13,7 +13,7 @@ const LoginFormBox = styled(Box)(({ theme }) => ({
backgroundSize: "cover", backgroundSize: "cover",
[`& .card`]: { [`& .card`]: {
width: "30rem", maxWidth: "30rem",
marginTop: "6rem", marginTop: "6rem",
marginBottom: "6rem", marginBottom: "6rem",
}, },

View File

@@ -8,6 +8,7 @@ import ErrorIcon from '@mui/icons-material/Error';
import { import {
Button, Button,
Datagrid, Datagrid,
DatagridConfigurable,
DateField, DateField,
List, List,
ListProps, ListProps,
@@ -123,14 +124,14 @@ export const DestinationList = (props: ListProps) => {
pagination={<DestinationPagination />} pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }} sort={{ field: "destination", order: "ASC" }}
> >
<Datagrid rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> <DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<FunctionField source="destination" render={destinationFieldRender} /> <FunctionField source="destination" render={destinationFieldRender} />
<DateField source="failure_ts" showTime options={DATE_FORMAT} /> <DateField source="failure_ts" showTime options={DATE_FORMAT} />
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} /> <RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
<TextField source="retry_interval" /> <TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" /> <TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton /> <DestinationReconnectButton />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );
}; };

View File

@@ -4,6 +4,7 @@ import {
Create, Create,
CreateProps, CreateProps,
Datagrid, Datagrid,
DatagridConfigurable,
DateField, DateField,
DateTimeInput, DateTimeInput,
Edit, Edit,
@@ -39,13 +40,13 @@ export const RegistrationTokenList = (props: ListProps) => (
pagination={false} pagination={false}
perPage={500} perPage={500}
> >
<Datagrid rowClick="edit"> <DatagridConfigurable rowClick="edit">
<TextField source="token" sortable={false} /> <TextField source="token" sortable={false} />
<NumberField source="uses_allowed" sortable={false} /> <NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" sortable={false} /> <NumberField source="pending" sortable={false} />
<NumberField source="completed" sortable={false} /> <NumberField source="completed" sortable={false} />
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} /> <DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );

View File

@@ -3,6 +3,7 @@ import ViewListIcon from "@mui/icons-material/ViewList";
import ReportIcon from "@mui/icons-material/Warning"; import ReportIcon from "@mui/icons-material/Warning";
import { import {
Datagrid, Datagrid,
DatagridConfigurable,
DateField, DateField,
DeleteButton, DeleteButton,
List, List,
@@ -90,13 +91,13 @@ const ReportShowActions = () => {
export const ReportList = (props: ListProps) => ( export const ReportList = (props: ListProps) => (
<List {...props} pagination={<ReportPagination />} sort={{ field: "received_ts", order: "DESC" }}> <List {...props} pagination={<ReportPagination />} sort={{ field: "received_ts", order: "DESC" }}>
<Datagrid rowClick="show" bulkActionButtons={false}> <DatagridConfigurable rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} /> <TextField source="id" sortable={false} />
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} /> <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
<TextField sortable={false} source="user_id" /> <TextField sortable={false} source="user_id" />
<TextField sortable={false} source="name" /> <TextField sortable={false} source="name" />
<TextField sortable={false} source="score" /> <TextField sortable={false} source="score" />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );

View File

@@ -1,6 +1,7 @@
import PermMediaIcon from "@mui/icons-material/PermMedia"; import PermMediaIcon from "@mui/icons-material/PermMedia";
import { import {
Datagrid, Datagrid,
DatagridConfigurable,
ExportButton, ExportButton,
List, List,
ListProps, ListProps,
@@ -37,12 +38,12 @@ export const UserMediaStatsList = (props: ListProps) => (
pagination={<UserMediaStatsPagination />} pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }} sort={{ field: "media_length", order: "DESC" }}
> >
<Datagrid rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}> <DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
<TextField source="user_id" label="resources.users.fields.id" /> <TextField source="user_id" label="resources.users.fields.id" />
<TextField source="displayname" label="resources.users.fields.displayname" /> <TextField source="displayname" label="resources.users.fields.displayname" />
<NumberField source="media_count" /> <NumberField source="media_count" />
<NumberField source="media_length" /> <NumberField source="media_length" />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );

View File

@@ -15,6 +15,7 @@ import {
ArrayField, ArrayField,
Button, Button,
Datagrid, Datagrid,
DatagridConfigurable,
DateField, DateField,
Create, Create,
CreateProps, CreateProps,
@@ -156,7 +157,7 @@ export const UserList = (props: ListProps) => (
actions={<UserListActions />} actions={<UserListActions />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
<Datagrid <DatagridConfigurable
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`} rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
bulkActionButtons={<UserBulkActionButtons />} bulkActionButtons={<UserBulkActionButtons />}
> >
@@ -169,7 +170,7 @@ export const UserList = (props: ListProps) => (
<BooleanField source="locked" /> <BooleanField source="locked" />
<BooleanField source="erased" sortable={false} /> <BooleanField source="erased" sortable={false} />
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} /> <DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
</Datagrid> </DatagridConfigurable>
</List> </List>
); );

View File

@@ -30,7 +30,7 @@ describe("authProvider", () => {
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/r0/login", { expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
@@ -61,7 +61,7 @@ describe("authProvider", () => {
}); });
expect(ret).toEqual({redirectTo: "/"}); expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", { expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}', body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
@@ -83,7 +83,7 @@ describe("authProvider", () => {
await authProvider.logout(null); await authProvider.logout(null);
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/r0/logout", { expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
Authorization: "Bearer foo", Authorization: "Bearer foo",

View File

@@ -2,6 +2,7 @@ import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
import storage from "../storage"; import storage from "../storage";
import { MatrixError, displayError } from "../components/error"; import { MatrixError, displayError } from "../components/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const authProvider: AuthProvider = { const authProvider: AuthProvider = {
// called when the user attempts to log in // called when the user attempts to log in
@@ -57,7 +58,7 @@ const authProvider: AuthProvider = {
storage.setItem("base_url", base_url); storage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url); const decoded_base_url = window.decodeURIComponent(base_url);
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login"); let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
let response; let response;
@@ -95,11 +96,48 @@ const authProvider: AuthProvider = {
); );
} }
}, },
getIdentity: async () => {
const access_token = storage.getItem("access_token");
const user_id = storage.getItem("user_id");
const base_url = storage.getItem("base_url");
if (typeof access_token !== "string" || typeof user_id !== "string" || typeof base_url !== "string") {
return Promise.reject();
}
const options: Options = {
headers: new Headers({
Accept: "application/json",
Authorization: `Bearer ${access_token}`,
}),
};
const whoami_api_url = base_url + `/_matrix/client/v3/profile/${user_id}`;
try {
let avatar_url = "";
const response = await fetchUtils.fetchJson(whoami_api_url, options);
if (response.json.avatar_url) {
const mediaresp = await fetchAuthenticatedMedia(response.json.avatar_url, "thumbnail");
const blob = await mediaresp.blob();
avatar_url = URL.createObjectURL(blob);
}
return Promise.resolve({
id: user_id,
fullName: response.json.displayname,
avatar: avatar_url,
});
} catch (err) {
console.log("Error getting identity", err);
return Promise.reject();
}
},
// called when the user clicks on the logout button // called when the user clicks on the logout button
logout: async () => { logout: async () => {
console.log("logout"); console.log("logout");
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/r0/logout"; const logout_api_url = storage.getItem("base_url") + "/_matrix/client/v3/logout";
const access_token = storage.getItem("access_token"); const access_token = storage.getItem("access_token");
const options: Options = { const options: Options = {