mirror of
				https://github.com/UA-Fediland/synapse-admin.git
				synced 2025-10-31 21:38:27 +00:00 
			
		
		
		
	Compare commits
	
		
			No commits in common. "7d4d765ab45cf0f5c5b07cd6ea3c6b1a7e5a5628" and "323ad9f9e29801251d18653d15c23772dc77cf63" have entirely different histories.
		
	
	
		
			7d4d765ab4
			...
			323ad9f9e2
		
	
		
					 40 changed files with 5972 additions and 4776 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/workflows/build-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/build-test.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -12,10 +12,10 @@ jobs: | |||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Setup node | ||||
|         uses: actions/setup-node@v4 | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: "18" | ||||
|       - name: Install dependencies | ||||
|         run: yarn --immutable | ||||
|         run: yarn --frozen-lockfile | ||||
|       - name: Run tests | ||||
|         run: yarn test | ||||
|  |  | |||
							
								
								
									
										8
									
								
								.github/workflows/docker-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/docker-release.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -19,11 +19,11 @@ jobs: | |||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Login to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|  | @ -43,7 +43,7 @@ jobs: | |||
|           esac | ||||
|           echo "::set-output name=tag::$tag" | ||||
|       - name: Build and Push Tag | ||||
|         uses: docker/build-push-action@v5 | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|  |  | |||
							
								
								
									
										6
									
								
								.github/workflows/edge_ghpage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/edge_ghpage.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -11,16 +11,16 @@ jobs: | |||
|     steps: | ||||
|       - name: Checkout 🛎️ | ||||
|         uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: "18" | ||||
|       - name: Install and Build 🔧 | ||||
|         run: | | ||||
|           yarn install --immutable | ||||
|           yarn install | ||||
|           yarn build | ||||
| 
 | ||||
|       - name: Deploy 🚀 | ||||
|         uses: JamesIves/github-pages-deploy-action@v4.5.0 | ||||
|         uses: JamesIves/github-pages-deploy-action@v4.4.3 | ||||
|         with: | ||||
|           branch: gh-pages | ||||
|           folder: build | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.github/workflows/github-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/github-release.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -14,10 +14,10 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-node@v4 | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: "18" | ||||
|       - run: yarn install --immutable | ||||
|       - run: yarn install | ||||
|       - run: yarn build | ||||
|       - run: | | ||||
|           version=`git describe --dirty --tags || echo unknown` | ||||
|  |  | |||
							
								
								
									
										8
									
								
								.github/workflows/test-docker-image.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/test-docker-image.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -19,11 +19,11 @@ jobs: | |||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Login to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|  | @ -43,7 +43,7 @@ jobs: | |||
|           esac | ||||
|           echo "::set-output name=tag::$tag" | ||||
|       - name: Build and Push Tag | ||||
|         uses: docker/build-push-action@v5 | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: . | ||||
|           push: false | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ ARG REACT_APP_SERVER | |||
| WORKDIR /src | ||||
| 
 | ||||
| COPY . /src | ||||
| RUN yarn --network-timeout=300000 install --immutable | ||||
| RUN yarn --network-timeout=300000 install | ||||
| RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,10 +13,10 @@ This project is built using [react-admin](https://marmelab.com/react-admin/). | |||
| 
 | ||||
| ### Supported Synapse | ||||
| 
 | ||||
| It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.0 for all functions to work as expected! | ||||
| It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.52.0 for all functions to work as expected! | ||||
| 
 | ||||
| You get your server version with the request `/_synapse/admin/v1/server_version`. | ||||
| See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html). | ||||
| See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html). | ||||
| 
 | ||||
| After entering the URL on the login page of synapse-admin the server version appears below the input field. | ||||
| 
 | ||||
|  | @ -27,7 +27,7 @@ You need access to the following endpoints: | |||
| - `/_matrix` | ||||
| - `/_synapse/admin` | ||||
| 
 | ||||
| See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints) | ||||
| See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints) | ||||
| 
 | ||||
| ### Use without install | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										33
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								package.json
									
									
									
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "synapse-admin", | ||||
|   "version": "0.9.1", | ||||
|   "version": "0.8.5", | ||||
|   "description": "Admin GUI for the Matrix.org server Synapse", | ||||
|   "author": "Awesome Technologies Innovationslabor GmbH", | ||||
|   "license": "Apache-2.0", | ||||
|  | @ -10,28 +10,31 @@ | |||
|     "url": "https://github.com/Awesome-Technologies/synapse-admin" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@testing-library/jest-dom": "^6.0.0", | ||||
|     "@testing-library/react": "^14.0.0", | ||||
|     "@testing-library/user-event": "^14.5.2", | ||||
|     "eslint": "^8.56.0", | ||||
|     "eslint-config-prettier": "^9.1.0", | ||||
|     "@testing-library/jest-dom": "^5.16.5", | ||||
|     "@testing-library/react": "^12.1.5", | ||||
|     "@testing-library/user-event": "^14.4.3", | ||||
|     "eslint": "^8.48.0", | ||||
|     "eslint-config-prettier": "^9.0.0", | ||||
|     "eslint-config-react-app": "^7.0.1", | ||||
|     "eslint-plugin-prettier": "^5.1.3", | ||||
|     "eslint-plugin-prettier": "^4.2.1", | ||||
|     "jest-fetch-mock": "^3.0.3", | ||||
|     "prettier": "^3.2.5" | ||||
|     "prettier": "^2.2.0", | ||||
|     "ra-test": "^3.19.12" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@mui/icons-material": "^5.15.10", | ||||
|     "@mui/material": "^5.15.10", | ||||
|     "@mui/styles": "^5.15.10", | ||||
|     "@emotion/react": "^11.11.1", | ||||
|     "@emotion/styled": "^11.10.6", | ||||
|     "@mui/icons-material": "^5.14.8", | ||||
|     "@mui/material": "^5.14.8", | ||||
|     "papaparse": "^5.4.1", | ||||
|     "prop-types": "^15.8.1", | ||||
|     "ra-language-chinese": "^2.0.10", | ||||
|     "ra-language-french": "^4.16.11", | ||||
|     "ra-language-french": "^4.13.3", | ||||
|     "ra-language-german": "^3.13.4", | ||||
|     "ra-language-italian": "^3.13.1", | ||||
|     "react": "^18.0.0", | ||||
|     "react-admin": "^4.16.11", | ||||
|     "react-dom": "^18.0.0", | ||||
|     "react": "^17.0.0", | ||||
|     "react-admin": "^3.19.12", | ||||
|     "react-dom": "^17.0.2", | ||||
|     "react-scripts": "^5.0.1" | ||||
|   }, | ||||
|   "scripts": { | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| id,displayname,password,is_guest,admin,deactivated | ||||
| testuser22,Jane Doe,secretpassword,false,true,false | ||||
| @testuser22:example.org,Jane Doe,secretpassword,false,true,false | ||||
| ,John Doe,,false,false,false | ||||
|  |  | |||
| 
 | 
							
								
								
									
										107
									
								
								src/App.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/App.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | |||
| import React from "react"; | ||||
| import { Admin, Resource, resolveBrowserLocale } from "react-admin"; | ||||
| import polyglotI18nProvider from "ra-i18n-polyglot"; | ||||
| import authProvider from "./synapse/authProvider"; | ||||
| import dataProvider from "./synapse/dataProvider"; | ||||
| import { UserList, UserCreate, UserEdit } from "./components/users"; | ||||
| import { RoomList, RoomShow } from "./components/rooms"; | ||||
| import { ReportList, ReportShow } from "./components/EventReports"; | ||||
| import LoginPage from "./components/LoginPage"; | ||||
| import ConfirmationNumberIcon from "@mui/icons-material/ConfirmationNumber"; | ||||
| import CloudQueueIcon from "@mui/icons-material/CloudQueue"; | ||||
| import EqualizerIcon from "@mui/icons-material/Equalizer"; | ||||
| import UserIcon from "@mui/icons-material/Group"; | ||||
| import { UserMediaStatsList } from "./components/statistics"; | ||||
| import RoomIcon from "@mui/icons-material/ViewList"; | ||||
| import ReportIcon from "@mui/icons-material/Warning"; | ||||
| import FolderSharedIcon from "@mui/icons-material/FolderShared"; | ||||
| import { DestinationList, DestinationShow } from "./components/destinations"; | ||||
| import { ImportFeature } from "./components/ImportFeature"; | ||||
| import { | ||||
|   RegistrationTokenCreate, | ||||
|   RegistrationTokenEdit, | ||||
|   RegistrationTokenList, | ||||
| } from "./components/RegistrationTokens"; | ||||
| import { RoomDirectoryList } from "./components/RoomDirectory"; | ||||
| import { Route } from "react-router-dom"; | ||||
| import germanMessages from "./i18n/de"; | ||||
| import englishMessages from "./i18n/en"; | ||||
| import frenchMessages from "./i18n/fr"; | ||||
| import chineseMessages from "./i18n/zh"; | ||||
| import italianMessages from "./i18n/it"; | ||||
| 
 | ||||
| // TODO: Can we use lazy loading together with browser locale?
 | ||||
| const messages = { | ||||
|   de: germanMessages, | ||||
|   en: englishMessages, | ||||
|   fr: frenchMessages, | ||||
|   it: italianMessages, | ||||
|   zh: chineseMessages, | ||||
| }; | ||||
| const i18nProvider = polyglotI18nProvider( | ||||
|   locale => (messages[locale] ? messages[locale] : messages.en), | ||||
|   resolveBrowserLocale() | ||||
| ); | ||||
| 
 | ||||
| const App = () => ( | ||||
|   <Admin | ||||
|     disableTelemetry | ||||
|     loginPage={LoginPage} | ||||
|     authProvider={authProvider} | ||||
|     dataProvider={dataProvider} | ||||
|     i18nProvider={i18nProvider} | ||||
|     customRoutes={[ | ||||
|       <Route key="userImport" path="/import_users" component={ImportFeature} />, | ||||
|     ]} | ||||
|   > | ||||
|     <Resource | ||||
|       name="users" | ||||
|       list={UserList} | ||||
|       create={UserCreate} | ||||
|       edit={UserEdit} | ||||
|       icon={UserIcon} | ||||
|     /> | ||||
|     <Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} /> | ||||
|     <Resource | ||||
|       name="user_media_statistics" | ||||
|       list={UserMediaStatsList} | ||||
|       icon={EqualizerIcon} | ||||
|     /> | ||||
|     <Resource | ||||
|       name="reports" | ||||
|       list={ReportList} | ||||
|       show={ReportShow} | ||||
|       icon={ReportIcon} | ||||
|     /> | ||||
|     <Resource | ||||
|       name="room_directory" | ||||
|       list={RoomDirectoryList} | ||||
|       icon={FolderSharedIcon} | ||||
|     /> | ||||
|     <Resource | ||||
|       name="destinations" | ||||
|       list={DestinationList} | ||||
|       show={DestinationShow} | ||||
|       icon={CloudQueueIcon} | ||||
|     /> | ||||
|     <Resource | ||||
|       name="registration_tokens" | ||||
|       list={RegistrationTokenList} | ||||
|       create={RegistrationTokenCreate} | ||||
|       edit={RegistrationTokenEdit} | ||||
|       icon={ConfirmationNumberIcon} | ||||
|     /> | ||||
|     <Resource name="connections" /> | ||||
|     <Resource name="devices" /> | ||||
|     <Resource name="room_members" /> | ||||
|     <Resource name="users_media" /> | ||||
|     <Resource name="joined_rooms" /> | ||||
|     <Resource name="pushers" /> | ||||
|     <Resource name="servernotices" /> | ||||
|     <Resource name="forward_extremities" /> | ||||
|     <Resource name="room_state" /> | ||||
|     <Resource name="destination_rooms" /> | ||||
|   </Admin> | ||||
| ); | ||||
| 
 | ||||
| export default App; | ||||
							
								
								
									
										72
									
								
								src/App.jsx
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/App.jsx
									
									
									
									
									
								
							|  | @ -1,72 +0,0 @@ | |||
| import React from "react"; | ||||
| import { | ||||
|   Admin, | ||||
|   CustomRoutes, | ||||
|   Resource, | ||||
|   resolveBrowserLocale, | ||||
| } from "react-admin"; | ||||
| import polyglotI18nProvider from "ra-i18n-polyglot"; | ||||
| import authProvider from "./synapse/authProvider"; | ||||
| import dataProvider from "./synapse/dataProvider"; | ||||
| import users from "./components/users"; | ||||
| import rooms from "./components/rooms"; | ||||
| import userMediaStats from "./components/statistics"; | ||||
| import reports from "./components/EventReports"; | ||||
| import roomDirectory from "./components/RoomDirectory"; | ||||
| import destinations from "./components/destinations"; | ||||
| import registrationToken from "./components/RegistrationTokens"; | ||||
| import LoginPage from "./components/LoginPage"; | ||||
| import { ImportFeature } from "./components/ImportFeature"; | ||||
| import { Route } from "react-router-dom"; | ||||
| import germanMessages from "./i18n/de"; | ||||
| import englishMessages from "./i18n/en"; | ||||
| import frenchMessages from "./i18n/fr"; | ||||
| import chineseMessages from "./i18n/zh"; | ||||
| import italianMessages from "./i18n/it"; | ||||
| 
 | ||||
| // TODO: Can we use lazy loading together with browser locale? | ||||
| const messages = { | ||||
|   de: germanMessages, | ||||
|   en: englishMessages, | ||||
|   fr: frenchMessages, | ||||
|   it: italianMessages, | ||||
|   zh: chineseMessages, | ||||
| }; | ||||
| const i18nProvider = polyglotI18nProvider( | ||||
|   locale => (messages[locale] ? messages[locale] : messages.en), | ||||
|   resolveBrowserLocale() | ||||
| ); | ||||
| 
 | ||||
| const App = () => ( | ||||
|   <Admin | ||||
|     disableTelemetry | ||||
|     requireAuth | ||||
|     loginPage={LoginPage} | ||||
|     authProvider={authProvider} | ||||
|     dataProvider={dataProvider} | ||||
|     i18nProvider={i18nProvider} | ||||
|   > | ||||
|     <CustomRoutes> | ||||
|       <Route path="/import_users" element={<ImportFeature />} /> | ||||
|     </CustomRoutes> | ||||
|     <Resource {...users} /> | ||||
|     <Resource {...rooms} /> | ||||
|     <Resource {...userMediaStats} /> | ||||
|     <Resource {...reports} /> | ||||
|     <Resource {...roomDirectory} /> | ||||
|     <Resource {...destinations} /> | ||||
|     <Resource {...registrationToken} /> | ||||
|     <Resource name="connections" /> | ||||
|     <Resource name="devices" /> | ||||
|     <Resource name="room_members" /> | ||||
|     <Resource name="users_media" /> | ||||
|     <Resource name="joined_rooms" /> | ||||
|     <Resource name="pushers" /> | ||||
|     <Resource name="servernotices" /> | ||||
|     <Resource name="forward_extremities" /> | ||||
|     <Resource name="room_state" /> | ||||
|     <Resource name="destination_rooms" /> | ||||
|   </Admin> | ||||
| ); | ||||
| 
 | ||||
| export default App; | ||||
|  | @ -1,22 +0,0 @@ | |||
| import React from "react"; | ||||
| import get from "lodash/get"; | ||||
| import { Avatar } from "@mui/material"; | ||||
| import { useRecordContext } from "react-admin"; | ||||
| 
 | ||||
| const AvatarField = ({ source, ...rest }) => { | ||||
|   const record = useRecordContext(rest); | ||||
|   const src = get(record, source)?.toString(); | ||||
|   const { alt, classes, sizes, sx, variant } = rest; | ||||
|   return ( | ||||
|     <Avatar | ||||
|       alt={alt} | ||||
|       classes={classes} | ||||
|       sizes={sizes} | ||||
|       src={src} | ||||
|       sx={sx} | ||||
|       variant={variant} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default AvatarField; | ||||
|  | @ -1,18 +0,0 @@ | |||
| import React from "react"; | ||||
| import { RecordContextProvider } from "react-admin"; | ||||
| import { render, screen } from "@testing-library/react"; | ||||
| import AvatarField from "./AvatarField"; | ||||
| 
 | ||||
| describe("AvatarField", () => { | ||||
|   it("shows image", () => { | ||||
|     const value = { | ||||
|       avatar: "foo", | ||||
|     }; | ||||
|     render( | ||||
|       <RecordContextProvider value={value}> | ||||
|         <AvatarField source="avatar" /> | ||||
|       </RecordContextProvider> | ||||
|     ); | ||||
|     expect(screen.getByRole("img").getAttribute("src")).toBe("foo"); | ||||
|   }); | ||||
| }); | ||||
|  | @ -2,7 +2,6 @@ import React from "react"; | |||
| import { | ||||
|   Datagrid, | ||||
|   DateField, | ||||
|   DeleteButton, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|  | @ -11,12 +10,9 @@ import { | |||
|   Tab, | ||||
|   TabbedShowLayout, | ||||
|   TextField, | ||||
|   TopToolbar, | ||||
|   useRecordContext, | ||||
|   useTranslate, | ||||
| } from "react-admin"; | ||||
| import PageviewIcon from "@mui/icons-material/Pageview"; | ||||
| import ReportIcon from "@mui/icons-material/Warning"; | ||||
| import ViewListIcon from "@mui/icons-material/ViewList"; | ||||
| 
 | ||||
| const date_format = { | ||||
|  | @ -28,14 +24,14 @@ const date_format = { | |||
|   second: "2-digit", | ||||
| }; | ||||
| 
 | ||||
| const ReportPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| const ReportPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| export const ReportShow = props => { | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|     <Show {...props} actions={<ReportShowActions />}> | ||||
|     <Show {...props}> | ||||
|       <TabbedShowLayout> | ||||
|         <Tab | ||||
|           label={translate("synapseadmin.reports.tabs.basic", { | ||||
|  | @ -94,7 +90,7 @@ export const ReportShow = props => { | |||
|           <TextField source="event_json.content.algorithm" /> | ||||
|           <TextField | ||||
|             source="event_json.content.device_id" | ||||
|             label="resources.devices.fields.device_id" | ||||
|             label="resources.users.fields.device_id" | ||||
|           /> | ||||
|         </Tab> | ||||
|       </TabbedShowLayout> | ||||
|  | @ -102,47 +98,26 @@ export const ReportShow = props => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const ReportShowActions = () => { | ||||
|   const record = useRecordContext(); | ||||
| 
 | ||||
| export const ReportList = ({ ...props }) => { | ||||
|   return ( | ||||
|     <TopToolbar> | ||||
|       <DeleteButton | ||||
|         record={record} | ||||
|         mutationMode="pessimistic" | ||||
|         confirmTitle="resources.reports.action.erase.title" | ||||
|         confirmContent="resources.reports.action.erase.content" | ||||
|       /> | ||||
|     </TopToolbar> | ||||
|     <List | ||||
|       {...props} | ||||
|       pagination={<ReportPagination />} | ||||
|       sort={{ field: "received_ts", order: "DESC" }} | ||||
|       bulkActionButtons={false} | ||||
|     > | ||||
|       <Datagrid rowClick="show"> | ||||
|         <TextField source="id" sortable={false} /> | ||||
|         <DateField | ||||
|           source="received_ts" | ||||
|           showTime | ||||
|           options={date_format} | ||||
|           sortable={true} | ||||
|         /> | ||||
|         <TextField sortable={false} source="user_id" /> | ||||
|         <TextField sortable={false} source="name" /> | ||||
|         <TextField sortable={false} source="score" /> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const ReportList = props => ( | ||||
|   <List | ||||
|     {...props} | ||||
|     pagination={<ReportPagination />} | ||||
|     sort={{ field: "received_ts", order: "DESC" }} | ||||
|   > | ||||
|     <Datagrid rowClick="show" bulkActionButtons={false}> | ||||
|       <TextField source="id" sortable={false} /> | ||||
|       <DateField | ||||
|         source="received_ts" | ||||
|         showTime | ||||
|         options={date_format} | ||||
|         sortable={true} | ||||
|       /> | ||||
|       <TextField sortable={false} source="user_id" /> | ||||
|       <TextField sortable={false} source="name" /> | ||||
|       <TextField sortable={false} source="score" /> | ||||
|     </Datagrid> | ||||
|   </List> | ||||
| ); | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "reports", | ||||
|   icon: ReportIcon, | ||||
|   list: ReportList, | ||||
|   show: ReportShow, | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
|  | @ -1,6 +1,12 @@ | |||
| import React, { useState } from "react"; | ||||
| import { useDataProvider, useNotify, Title } from "react-admin"; | ||||
| import { | ||||
|   Button as ReactAdminButton, | ||||
|   useDataProvider, | ||||
|   useNotify, | ||||
|   Title, | ||||
| } from "react-admin"; | ||||
| import { parse as parseCsv, unparse as unparseCsv } from "papaparse"; | ||||
| import GetAppIcon from "@mui/icons-material/GetApp"; | ||||
| import { | ||||
|   Button, | ||||
|   Card, | ||||
|  | @ -17,6 +23,19 @@ import { generateRandomUser } from "./users"; | |||
| 
 | ||||
| const LOGGING = true; | ||||
| 
 | ||||
| export const ImportButton = ({ label, variant = "text" }) => { | ||||
|   return ( | ||||
|     <ReactAdminButton | ||||
|       color="primary" | ||||
|       component="span" | ||||
|       variant={variant} | ||||
|       label={label} | ||||
|     > | ||||
|       <GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> | ||||
|     </ReactAdminButton> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const expectedFields = ["id", "displayname"].sort(); | ||||
| const optionalFields = [ | ||||
|   "user_type", | ||||
|  | @ -32,7 +51,7 @@ function TranslatableOption({ value, text }) { | |||
|   return <option value={value}>{translate(text)}</option>; | ||||
| } | ||||
| 
 | ||||
| const FilePicker = () => { | ||||
| const FilePicker = props => { | ||||
|   const [values, setValues] = useState(null); | ||||
|   const [error, setError] = useState(null); | ||||
|   const [stats, setStats] = useState(null); | ||||
|  | @ -191,7 +210,7 @@ const FilePicker = () => { | |||
|     return true; | ||||
|   }; | ||||
| 
 | ||||
|   const runImport = async _e => { | ||||
|   const runImport = async e => { | ||||
|     if (progress !== null) { | ||||
|       notify("import_users.errors.already_in_progress"); | ||||
|       return; | ||||
|  | @ -307,7 +326,7 @@ const FilePicker = () => { | |||
|         let retries = 0; | ||||
|         const submitRecord = recordData => { | ||||
|           return dataProvider.getOne("users", { id: recordData.id }).then( | ||||
|             async _alreadyExists => { | ||||
|             async alreadyExists => { | ||||
|               if (LOGGING) console.log("already existed"); | ||||
| 
 | ||||
|               if (useridMode === "update" || conflictMode === "skip") { | ||||
|  | @ -332,7 +351,7 @@ const FilePicker = () => { | |||
|                 } | ||||
|               } | ||||
|             }, | ||||
|             async _okToSubmit => { | ||||
|             async okToSubmit => { | ||||
|               if (LOGGING) | ||||
|                 console.log( | ||||
|                   "OK to create record " + | ||||
							
								
								
									
										378
									
								
								src/components/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								src/components/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,378 @@ | |||
| import React, { useState, useEffect } from "react"; | ||||
| import { | ||||
|   fetchUtils, | ||||
|   FormDataConsumer, | ||||
|   Notification, | ||||
|   useLogin, | ||||
|   useNotify, | ||||
|   useLocale, | ||||
|   useSetLocale, | ||||
|   useTranslate, | ||||
|   PasswordInput, | ||||
|   TextInput, | ||||
| } from "react-admin"; | ||||
| import { Form, useForm } from "react-final-form"; | ||||
| import { | ||||
|   Avatar, | ||||
|   Button, | ||||
|   Card, | ||||
|   CardActions, | ||||
|   CircularProgress, | ||||
|   MenuItem, | ||||
|   Select, | ||||
|   TextField, | ||||
| } from "@mui/material"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| import LockIcon from "@mui/icons-material/Lock"; | ||||
| 
 | ||||
| const useStyles = makeStyles(theme => ({ | ||||
|   main: { | ||||
|     display: "flex", | ||||
|     flexDirection: "column", | ||||
|     minHeight: "calc(100vh - 1em)", | ||||
|     alignItems: "center", | ||||
|     justifyContent: "flex-start", | ||||
|     background: "url(./images/floating-cogs.svg)", | ||||
|     backgroundColor: "#f9f9f9", | ||||
|     backgroundRepeat: "no-repeat", | ||||
|     backgroundSize: "cover", | ||||
|   }, | ||||
|   card: { | ||||
|     minWidth: "30em", | ||||
|     marginTop: "6em", | ||||
|     marginBottom: "6em", | ||||
|   }, | ||||
|   avatar: { | ||||
|     margin: "1em", | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|   }, | ||||
|   icon: { | ||||
|     backgroundColor: theme.palette.secondary.main, | ||||
|   }, | ||||
|   hint: { | ||||
|     marginTop: "1em", | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|     color: theme.palette.grey[500], | ||||
|   }, | ||||
|   form: { | ||||
|     padding: "0 1em 1em 1em", | ||||
|   }, | ||||
|   input: { | ||||
|     marginTop: "1em", | ||||
|   }, | ||||
|   actions: { | ||||
|     padding: "0 1em 1em 1em", | ||||
|   }, | ||||
|   serverVersion: { | ||||
|     color: "#9e9e9e", | ||||
|     fontFamily: "Roboto, Helvetica, Arial, sans-serif", | ||||
|     marginBottom: "1em", | ||||
|     marginLeft: "0.5em", | ||||
|   }, | ||||
| })); | ||||
| 
 | ||||
| const LoginPage = ({ theme }) => { | ||||
|   const classes = useStyles({ theme }); | ||||
|   const login = useLogin(); | ||||
|   const notify = useNotify(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [supportPassAuth, setSupportPassAuth] = useState(true); | ||||
|   var locale = useLocale(); | ||||
|   const setLocale = useSetLocale(); | ||||
|   const translate = useTranslate(); | ||||
|   const base_url = localStorage.getItem("base_url"); | ||||
|   const cfg_base_url = process.env.REACT_APP_SERVER; | ||||
|   const [ssoBaseUrl, setSSOBaseUrl] = useState(""); | ||||
|   const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href); | ||||
| 
 | ||||
|   if (loginToken) { | ||||
|     const ssoToken = loginToken[1]; | ||||
|     console.log("SSO token is", ssoToken); | ||||
|     // Prevent further requests
 | ||||
|     window.history.replaceState( | ||||
|       {}, | ||||
|       "", | ||||
|       window.location.href.replace(loginToken[0], "#").split("#")[0] | ||||
|     ); | ||||
|     const baseUrl = localStorage.getItem("sso_base_url"); | ||||
|     localStorage.removeItem("sso_base_url"); | ||||
|     if (baseUrl) { | ||||
|       const auth = { | ||||
|         base_url: baseUrl, | ||||
|         username: null, | ||||
|         password: null, | ||||
|         loginToken: ssoToken, | ||||
|       }; | ||||
|       console.log("Base URL is:", baseUrl); | ||||
|       console.log("SSO Token is:", ssoToken); | ||||
|       console.log("Let's try token login..."); | ||||
|       login(auth).catch(error => { | ||||
|         alert( | ||||
|           typeof error === "string" | ||||
|             ? error | ||||
|             : typeof error === "undefined" || !error.message | ||||
|             ? "ra.auth.sign_in_error" | ||||
|             : error.message | ||||
|         ); | ||||
|         console.error(error); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const renderInput = ({ | ||||
|     meta: { touched, error } = {}, | ||||
|     input: { ...inputProps }, | ||||
|     ...props | ||||
|   }) => ( | ||||
|     <TextField | ||||
|       error={!!(touched && error)} | ||||
|       helperText={touched && error} | ||||
|       {...inputProps} | ||||
|       {...props} | ||||
|       fullWidth | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   const validate = values => { | ||||
|     const errors = {}; | ||||
|     if (!values.username) { | ||||
|       errors.username = translate("ra.validation.required"); | ||||
|     } | ||||
|     if (!values.password) { | ||||
|       errors.password = translate("ra.validation.required"); | ||||
|     } | ||||
|     if (!values.base_url) { | ||||
|       errors.base_url = translate("ra.validation.required"); | ||||
|     } else { | ||||
|       if (!values.base_url.match(/^(http|https):\/\//)) { | ||||
|         errors.base_url = translate("synapseadmin.auth.protocol_error"); | ||||
|       } else if ( | ||||
|         !values.base_url.match( | ||||
|           /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/ | ||||
|         ) | ||||
|       ) { | ||||
|         errors.base_url = translate("synapseadmin.auth.url_error"); | ||||
|       } | ||||
|     } | ||||
|     return errors; | ||||
|   }; | ||||
| 
 | ||||
|   const handleSubmit = auth => { | ||||
|     setLoading(true); | ||||
|     login(auth).catch(error => { | ||||
|       setLoading(false); | ||||
|       notify( | ||||
|         typeof error === "string" | ||||
|           ? error | ||||
|           : typeof error === "undefined" || !error.message | ||||
|           ? "ra.auth.sign_in_error" | ||||
|           : error.message, | ||||
|         { type: "warning" } | ||||
|       ); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const handleSSO = () => { | ||||
|     localStorage.setItem("sso_base_url", ssoBaseUrl); | ||||
|     const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent( | ||||
|       window.location.href | ||||
|     )}`;
 | ||||
|     window.location.href = ssoFullUrl; | ||||
|   }; | ||||
| 
 | ||||
|   const extractHomeServer = username => { | ||||
|     const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/; | ||||
|     if (!username) return null; | ||||
|     const res = username.match(usernameRegex); | ||||
|     if (res) return res[1]; | ||||
|     return null; | ||||
|   }; | ||||
| 
 | ||||
|   const UserData = ({ formData }) => { | ||||
|     const form = useForm(); | ||||
|     const [serverVersion, setServerVersion] = useState(""); | ||||
| 
 | ||||
|     const handleUsernameChange = _ => { | ||||
|       if (formData.base_url || cfg_base_url) return; | ||||
|       // check if username is a full qualified userId then set base_url accordially
 | ||||
|       const home_server = extractHomeServer(formData.username); | ||||
|       const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`; | ||||
|       if (home_server) { | ||||
|         // fetch .well-known entry to get base_url
 | ||||
|         fetchUtils | ||||
|           .fetchJson(wellKnownUrl, { method: "GET" }) | ||||
|           .then(({ json }) => { | ||||
|             form.change("base_url", json["m.homeserver"].base_url); | ||||
|           }) | ||||
|           .catch(_ => { | ||||
|             // if there is no .well-known entry, try the home server name
 | ||||
|             form.change("base_url", `https://${home_server}`); | ||||
|           }); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     useEffect( | ||||
|       _ => { | ||||
|         if ( | ||||
|           !formData.base_url || | ||||
|           !formData.base_url.match( | ||||
|             /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/ | ||||
|           ) | ||||
|         ) | ||||
|           return; | ||||
|         const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`; | ||||
|         fetchUtils | ||||
|           .fetchJson(versionUrl, { method: "GET" }) | ||||
|           .then(({ json }) => { | ||||
|             setServerVersion( | ||||
|               `${translate("synapseadmin.auth.server_version")} ${ | ||||
|                 json["server_version"] | ||||
|               }` | ||||
|             ); | ||||
|           }) | ||||
|           .catch(_ => { | ||||
|             setServerVersion(""); | ||||
|           }); | ||||
| 
 | ||||
|         // Set SSO Url
 | ||||
|         const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`; | ||||
|         let supportPass = false, | ||||
|           supportSSO = false; | ||||
|         fetchUtils | ||||
|           .fetchJson(authMethodUrl, { method: "GET" }) | ||||
|           .then(({ json }) => { | ||||
|             json.flows.forEach(f => { | ||||
|               if (f.type === "m.login.password") { | ||||
|                 supportPass = true; | ||||
|               } else if (f.type === "m.login.sso") { | ||||
|                 supportSSO = true; | ||||
|               } | ||||
|             }); | ||||
|             setSupportPassAuth(supportPass); | ||||
|             if (supportSSO) { | ||||
|               setSSOBaseUrl(formData.base_url); | ||||
|             } else { | ||||
|               setSSOBaseUrl(""); | ||||
|             } | ||||
|           }) | ||||
|           .catch(_ => { | ||||
|             setSSOBaseUrl(""); | ||||
|           }); | ||||
|       }, | ||||
|       [formData.base_url] | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         <div className={classes.input}> | ||||
|           <TextInput | ||||
|             autoFocus | ||||
|             name="username" | ||||
|             component={renderInput} | ||||
|             label="ra.auth.username" | ||||
|             disabled={loading || !supportPassAuth} | ||||
|             onBlur={handleUsernameChange} | ||||
|             resettable | ||||
|             fullWidth | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={classes.input}> | ||||
|           <PasswordInput | ||||
|             name="password" | ||||
|             component={renderInput} | ||||
|             label="ra.auth.password" | ||||
|             type="password" | ||||
|             disabled={loading || !supportPassAuth} | ||||
|             resettable | ||||
|             fullWidth | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={classes.input}> | ||||
|           <TextInput | ||||
|             name="base_url" | ||||
|             component={renderInput} | ||||
|             label="synapseadmin.auth.base_url" | ||||
|             disabled={cfg_base_url || loading} | ||||
|             resettable | ||||
|             fullWidth | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={classes.serverVersion}>{serverVersion}</div> | ||||
|       </div> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Form | ||||
|       initialValues={{ base_url: cfg_base_url || base_url }} | ||||
|       onSubmit={handleSubmit} | ||||
|       validate={validate} | ||||
|       render={({ handleSubmit }) => ( | ||||
|         <form onSubmit={handleSubmit} noValidate> | ||||
|           <div className={classes.main}> | ||||
|             <Card className={classes.card}> | ||||
|               <div className={classes.avatar}> | ||||
|                 <Avatar className={classes.icon}> | ||||
|                   <LockIcon /> | ||||
|                 </Avatar> | ||||
|               </div> | ||||
|               <div className={classes.hint}> | ||||
|                 {translate("synapseadmin.auth.welcome")} | ||||
|               </div> | ||||
|               <div className={classes.form}> | ||||
|                 <div className={classes.input}> | ||||
|                   <Select | ||||
|                     value={locale} | ||||
|                     onChange={e => { | ||||
|                       setLocale(e.target.value); | ||||
|                     }} | ||||
|                     fullWidth | ||||
|                     disabled={loading} | ||||
|                   > | ||||
|                     <MenuItem value="de">Deutsch</MenuItem> | ||||
|                     <MenuItem value="en">English</MenuItem> | ||||
|                     <MenuItem value="fr">Français</MenuItem> | ||||
|                     <MenuItem value="it">Italiano</MenuItem> | ||||
|                     <MenuItem value="zh">简体中文</MenuItem> | ||||
|                   </Select> | ||||
|                 </div> | ||||
|                 <FormDataConsumer> | ||||
|                   {formDataProps => <UserData {...formDataProps} />} | ||||
|                 </FormDataConsumer> | ||||
|               </div> | ||||
|               <CardActions className={classes.actions}> | ||||
|                 <Button | ||||
|                   variant="contained" | ||||
|                   type="submit" | ||||
|                   color="primary" | ||||
|                   disabled={loading || !supportPassAuth} | ||||
|                   className={classes.button} | ||||
|                   fullWidth | ||||
|                 > | ||||
|                   {loading && <CircularProgress size={25} thickness={2} />} | ||||
|                   {translate("ra.auth.sign_in")} | ||||
|                 </Button> | ||||
|                 <Button | ||||
|                   variant="contained" | ||||
|                   color="secondary" | ||||
|                   onClick={handleSSO} | ||||
|                   disabled={loading || ssoBaseUrl === ""} | ||||
|                   className={classes.button} | ||||
|                   fullWidth | ||||
|                 > | ||||
|                   {loading && <CircularProgress size={25} thickness={2} />} | ||||
|                   {translate("synapseadmin.auth.sso_sign_in")} | ||||
|                 </Button> | ||||
|               </CardActions> | ||||
|             </Card> | ||||
|             <Notification /> | ||||
|           </div> | ||||
|         </form> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default LoginPage; | ||||
|  | @ -1,327 +0,0 @@ | |||
| import React, { useState, useEffect } from "react"; | ||||
| import { | ||||
|   Form, | ||||
|   FormDataConsumer, | ||||
|   Notification, | ||||
|   required, | ||||
|   useLogin, | ||||
|   useNotify, | ||||
|   useLocaleState, | ||||
|   useTranslate, | ||||
|   PasswordInput, | ||||
|   TextInput, | ||||
| } from "react-admin"; | ||||
| import { useFormContext } from "react-hook-form"; | ||||
| import { | ||||
|   Avatar, | ||||
|   Box, | ||||
|   Button, | ||||
|   Card, | ||||
|   CardActions, | ||||
|   CircularProgress, | ||||
|   MenuItem, | ||||
|   Select, | ||||
|   TextField, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import { styled } from "@mui/material/styles"; | ||||
| import LockIcon from "@mui/icons-material/Lock"; | ||||
| import { | ||||
|   getServerVersion, | ||||
|   getSupportedLoginFlows, | ||||
|   getWellKnownUrl, | ||||
|   isValidBaseUrl, | ||||
|   splitMxid, | ||||
| } from "../synapse/synapse"; | ||||
| 
 | ||||
| const FormBox = styled(Box)(({ theme }) => ({ | ||||
|   display: "flex", | ||||
|   flexDirection: "column", | ||||
|   minHeight: "calc(100vh - 1em)", | ||||
|   alignItems: "center", | ||||
|   justifyContent: "flex-start", | ||||
|   background: "url(./images/floating-cogs.svg)", | ||||
|   backgroundColor: "#f9f9f9", | ||||
|   backgroundRepeat: "no-repeat", | ||||
|   backgroundSize: "cover", | ||||
| 
 | ||||
|   [`& .card`]: { | ||||
|     minWidth: "30em", | ||||
|     marginTop: "6em", | ||||
|     marginBottom: "6em", | ||||
|   }, | ||||
|   [`& .avatar`]: { | ||||
|     margin: "1em", | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|   }, | ||||
|   [`& .icon`]: { | ||||
|     backgroundColor: theme.palette.grey[500], | ||||
|   }, | ||||
|   [`& .hint`]: { | ||||
|     marginTop: "1em", | ||||
|     display: "flex", | ||||
|     justifyContent: "center", | ||||
|     color: theme.palette.grey[600], | ||||
|   }, | ||||
|   [`& .form`]: { | ||||
|     padding: "0 1em 1em 1em", | ||||
|   }, | ||||
|   [`& .input`]: { | ||||
|     marginTop: "1em", | ||||
|   }, | ||||
|   [`& .actions`]: { | ||||
|     padding: "0 1em 1em 1em", | ||||
|   }, | ||||
|   [`& .serverVersion`]: { | ||||
|     color: theme.palette.grey[500], | ||||
|     fontFamily: "Roboto, Helvetica, Arial, sans-serif", | ||||
|     marginBottom: "1em", | ||||
|     marginLeft: "0.5em", | ||||
|   }, | ||||
| })); | ||||
| 
 | ||||
| const LoginPage = () => { | ||||
|   const login = useLogin(); | ||||
|   const notify = useNotify(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [supportPassAuth, setSupportPassAuth] = useState(true); | ||||
|   const [locale, setLocale] = useLocaleState(); | ||||
|   const translate = useTranslate(); | ||||
|   const base_url = localStorage.getItem("base_url"); | ||||
|   const cfg_base_url = process.env.REACT_APP_SERVER; | ||||
|   const [ssoBaseUrl, setSSOBaseUrl] = useState(""); | ||||
|   const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href); | ||||
| 
 | ||||
|   if (loginToken) { | ||||
|     const ssoToken = loginToken[1]; | ||||
|     console.log("SSO token is", ssoToken); | ||||
|     // Prevent further requests | ||||
|     window.history.replaceState( | ||||
|       {}, | ||||
|       "", | ||||
|       window.location.href.replace(loginToken[0], "#").split("#")[0] | ||||
|     ); | ||||
|     const baseUrl = localStorage.getItem("sso_base_url"); | ||||
|     localStorage.removeItem("sso_base_url"); | ||||
|     if (baseUrl) { | ||||
|       const auth = { | ||||
|         base_url: baseUrl, | ||||
|         username: null, | ||||
|         password: null, | ||||
|         loginToken: ssoToken, | ||||
|       }; | ||||
|       console.log("Base URL is:", baseUrl); | ||||
|       console.log("SSO Token is:", ssoToken); | ||||
|       console.log("Let's try token login..."); | ||||
|       login(auth).catch(error => { | ||||
|         alert( | ||||
|           typeof error === "string" | ||||
|             ? error | ||||
|             : typeof error === "undefined" || !error.message | ||||
|               ? "ra.auth.sign_in_error" | ||||
|               : error.message | ||||
|         ); | ||||
|         console.error(error); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const renderInput = ({ | ||||
|     meta: { touched, error } = {}, | ||||
|     input: { ...inputProps }, | ||||
|     ...props | ||||
|   }) => ( | ||||
|     <TextField | ||||
|       error={!!(touched && error)} | ||||
|       helperText={touched && error} | ||||
|       {...inputProps} | ||||
|       {...props} | ||||
|       fullWidth | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   const validateBaseUrl = value => { | ||||
|     if (!value.match(/^(http|https):\/\//)) { | ||||
|       return translate("synapseadmin.auth.protocol_error"); | ||||
|     } else if ( | ||||
|       !value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/) | ||||
|     ) { | ||||
|       return translate("synapseadmin.auth.url_error"); | ||||
|     } else { | ||||
|       return undefined; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleSubmit = auth => { | ||||
|     setLoading(true); | ||||
|     login(auth).catch(error => { | ||||
|       setLoading(false); | ||||
|       notify( | ||||
|         typeof error === "string" | ||||
|           ? error | ||||
|           : typeof error === "undefined" || !error.message | ||||
|             ? "ra.auth.sign_in_error" | ||||
|             : error.message, | ||||
|         { type: "warning" } | ||||
|       ); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const handleSSO = () => { | ||||
|     localStorage.setItem("sso_base_url", ssoBaseUrl); | ||||
|     const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent( | ||||
|       window.location.href | ||||
|     )}`; | ||||
|     window.location.href = ssoFullUrl; | ||||
|   }; | ||||
| 
 | ||||
|   const UserData = ({ formData }) => { | ||||
|     const form = useFormContext(); | ||||
|     const [serverVersion, setServerVersion] = useState(""); | ||||
| 
 | ||||
|     const handleUsernameChange = _ => { | ||||
|       if (formData.base_url || cfg_base_url) return; | ||||
|       // check if username is a full qualified userId then set base_url accordingly | ||||
|       const domain = splitMxid(formData.username)?.domain; | ||||
|       if (domain) { | ||||
|         getWellKnownUrl(domain).then(url => form.setValue("base_url", url)); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|       if (!isValidBaseUrl(formData.base_url)) return; | ||||
| 
 | ||||
|       getServerVersion(formData.base_url) | ||||
|         .then(serverVersion => | ||||
|           setServerVersion( | ||||
|             `${translate("synapseadmin.auth.server_version")} ${serverVersion}` | ||||
|           ) | ||||
|         ) | ||||
|         .catch(() => setServerVersion("")); | ||||
| 
 | ||||
|       // Set SSO Url | ||||
|       getSupportedLoginFlows(formData.base_url) | ||||
|         .then(loginFlows => { | ||||
|           const supportPass = | ||||
|             loginFlows.find(f => f.type === "m.login.password") !== undefined; | ||||
|           const supportSSO = | ||||
|             loginFlows.find(f => f.type === "m.login.sso") !== undefined; | ||||
|           setSupportPassAuth(supportPass); | ||||
|           setSSOBaseUrl(supportSSO ? formData.base_url : ""); | ||||
|         }) | ||||
|         .catch(() => setSSOBaseUrl("")); | ||||
|     }, [formData.base_url]); | ||||
| 
 | ||||
|     return ( | ||||
|       <> | ||||
|         <Box> | ||||
|           <TextInput | ||||
|             autoFocus | ||||
|             name="username" | ||||
|             component={renderInput} | ||||
|             label="ra.auth.username" | ||||
|             disabled={loading || !supportPassAuth} | ||||
|             onBlur={handleUsernameChange} | ||||
|             resettable | ||||
|             fullWidth | ||||
|             className="input" | ||||
|             validate={required()} | ||||
|           /> | ||||
|         </Box> | ||||
|         <Box> | ||||
|           <PasswordInput | ||||
|             name="password" | ||||
|             component={renderInput} | ||||
|             label="ra.auth.password" | ||||
|             type="password" | ||||
|             disabled={loading || !supportPassAuth} | ||||
|             resettable | ||||
|             fullWidth | ||||
|             className="input" | ||||
|             validate={required()} | ||||
|           /> | ||||
|         </Box> | ||||
|         <Box> | ||||
|           <TextInput | ||||
|             name="base_url" | ||||
|             component={renderInput} | ||||
|             label="synapseadmin.auth.base_url" | ||||
|             disabled={cfg_base_url || loading} | ||||
|             resettable | ||||
|             fullWidth | ||||
|             className="input" | ||||
|             validate={[required(), validateBaseUrl]} | ||||
|           /> | ||||
|         </Box> | ||||
|         <Typography className="serverVersion">{serverVersion}</Typography> | ||||
|       </> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Form | ||||
|       defaultValues={{ base_url: cfg_base_url || base_url }} | ||||
|       onSubmit={handleSubmit} | ||||
|       mode="onTouched" | ||||
|     > | ||||
|       <FormBox> | ||||
|         <Card className="card"> | ||||
|           <Box className="avatar"> | ||||
|             {loading ? ( | ||||
|               <CircularProgress size={25} thickness={2} /> | ||||
|             ) : ( | ||||
|               <Avatar className="icon"> | ||||
|                 <LockIcon /> | ||||
|               </Avatar> | ||||
|             )} | ||||
|           </Box> | ||||
|           <Box className="hint">{translate("synapseadmin.auth.welcome")}</Box> | ||||
|           <Box className="form"> | ||||
|             <Select | ||||
|               value={locale} | ||||
|               onChange={e => { | ||||
|                 setLocale(e.target.value); | ||||
|               }} | ||||
|               fullWidth | ||||
|               disabled={loading} | ||||
|               className="input" | ||||
|             > | ||||
|               <MenuItem value="de">Deutsch</MenuItem> | ||||
|               <MenuItem value="en">English</MenuItem> | ||||
|               <MenuItem value="fr">Français</MenuItem> | ||||
|               <MenuItem value="it">Italiano</MenuItem> | ||||
|               <MenuItem value="zh">简体中文</MenuItem> | ||||
|             </Select> | ||||
|             <FormDataConsumer> | ||||
|               {formDataProps => <UserData {...formDataProps} />} | ||||
|             </FormDataConsumer> | ||||
|             <CardActions className="actions"> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 type="submit" | ||||
|                 color="primary" | ||||
|                 disabled={loading || !supportPassAuth} | ||||
|                 fullWidth | ||||
|               > | ||||
|                 {translate("ra.auth.sign_in")} | ||||
|               </Button> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 color="secondary" | ||||
|                 onClick={handleSSO} | ||||
|                 disabled={loading || ssoBaseUrl === ""} | ||||
|                 fullWidth | ||||
|               > | ||||
|                 {translate("synapseadmin.auth.sso_sign_in")} | ||||
|               </Button> | ||||
|             </CardActions> | ||||
|           </Box> | ||||
|         </Card> | ||||
|       </FormBox> | ||||
|       <Notification /> | ||||
|     </Form> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default LoginPage; | ||||
|  | @ -1,14 +1,14 @@ | |||
| import React from "react"; | ||||
| import { render } from "@testing-library/react"; | ||||
| import { AdminContext } from "react-admin"; | ||||
| import { TestContext } from "ra-test"; | ||||
| import LoginPage from "./LoginPage"; | ||||
| 
 | ||||
| describe("LoginForm", () => { | ||||
|   it("renders", () => { | ||||
|     render( | ||||
|       <AdminContext> | ||||
|       <TestContext> | ||||
|         <LoginPage /> | ||||
|       </AdminContext> | ||||
|       </TestContext> | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										39
									
								
								src/components/Menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/components/Menu.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| // in src/Menu.js
 | ||||
| import * as React from "react"; | ||||
| import { useSelector } from "react-redux"; | ||||
| import { useMediaQuery } from "@mui/material"; | ||||
| import { MenuItemLink, getResources } from "react-admin"; | ||||
| import DefaultIcon from "@mui/icons-material/ViewList"; | ||||
| import LabelIcon from "@mui/icons-material/Label"; | ||||
| 
 | ||||
| const Menu = ({ onMenuClick, logout }) => { | ||||
|   const isXSmall = useMediaQuery(theme => theme.breakpoints.down("xs")); | ||||
|   const open = useSelector(state => state.admin.ui.sidebarOpen); | ||||
|   const resources = useSelector(getResources); | ||||
|   return ( | ||||
|     <div> | ||||
|       {resources.map(resource => ( | ||||
|         <MenuItemLink | ||||
|           key={resource.name} | ||||
|           to={`/${resource.name}`} | ||||
|           primaryText={ | ||||
|             (resource.options && resource.options.label) || resource.name | ||||
|           } | ||||
|           leftIcon={resource.icon ? <resource.icon /> : <DefaultIcon />} | ||||
|           onClick={onMenuClick} | ||||
|           sidebarIsOpen={open} | ||||
|         /> | ||||
|       ))} | ||||
|       <MenuItemLink | ||||
|         to="/custom-route" | ||||
|         primaryText="Miscellaneous" | ||||
|         leftIcon={<LabelIcon />} | ||||
|         onClick={onMenuClick} | ||||
|         sidebarIsOpen={open} | ||||
|       /> | ||||
|       {isXSmall && logout} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Menu; | ||||
|  | @ -6,19 +6,18 @@ import { | |||
|   DateField, | ||||
|   DateTimeInput, | ||||
|   Edit, | ||||
|   Filter, | ||||
|   List, | ||||
|   maxValue, | ||||
|   number, | ||||
|   NumberField, | ||||
|   NumberInput, | ||||
|   regex, | ||||
|   SaveButton, | ||||
|   SimpleForm, | ||||
|   TextInput, | ||||
|   TextField, | ||||
|   Toolbar, | ||||
| } from "react-admin"; | ||||
| import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber"; | ||||
| 
 | ||||
| const date_format = { | ||||
|   year: "numeric", | ||||
|  | @ -54,41 +53,40 @@ const dateFormatter = v => { | |||
|   return `${year}-${month}-${day}T${hour}:${minute}`; | ||||
| }; | ||||
| 
 | ||||
| const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />]; | ||||
| 
 | ||||
| export const RegistrationTokenList = props => ( | ||||
|   <List | ||||
|     {...props} | ||||
|     filters={registrationTokenFilters} | ||||
|     filterDefaultValues={{ valid: true }} | ||||
|     pagination={false} | ||||
|     perPage={500} | ||||
|   > | ||||
|     <Datagrid rowClick="edit"> | ||||
|       <TextField source="token" sortable={false} /> | ||||
|       <NumberField source="uses_allowed" sortable={false} /> | ||||
|       <NumberField source="pending" sortable={false} /> | ||||
|       <NumberField source="completed" sortable={false} /> | ||||
|       <DateField | ||||
|         source="expiry_time" | ||||
|         showTime | ||||
|         options={date_format} | ||||
|         sortable={false} | ||||
|       /> | ||||
|     </Datagrid> | ||||
|   </List> | ||||
| const RegistrationTokenFilter = props => ( | ||||
|   <Filter {...props}> | ||||
|     <BooleanInput source="valid" alwaysOn /> | ||||
|   </Filter> | ||||
| ); | ||||
| 
 | ||||
| export const RegistrationTokenCreate = props => ( | ||||
|   <Create {...props} redirect="list"> | ||||
|     <SimpleForm | ||||
|       toolbar={ | ||||
|         <Toolbar> | ||||
|           {/* It is possible to create tokens per default without input. */} | ||||
|           <SaveButton alwaysEnable /> | ||||
|         </Toolbar> | ||||
|       } | ||||
| export const RegistrationTokenList = props => { | ||||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       filters={<RegistrationTokenFilter />} | ||||
|       filterDefaultValues={{ valid: true }} | ||||
|       pagination={false} | ||||
|       perPage={500} | ||||
|     > | ||||
|       <Datagrid rowClick="edit"> | ||||
|         <TextField source="token" sortable={false} /> | ||||
|         <NumberField source="uses_allowed" sortable={false} /> | ||||
|         <NumberField source="pending" sortable={false} /> | ||||
|         <NumberField source="completed" sortable={false} /> | ||||
|         <DateField | ||||
|           source="expiry_time" | ||||
|           showTime | ||||
|           options={date_format} | ||||
|           sortable={false} | ||||
|         /> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RegistrationTokenCreate = props => ( | ||||
|   <Create {...props}> | ||||
|     <SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}> | ||||
|       <TextInput | ||||
|         source="token" | ||||
|         autoComplete="off" | ||||
|  | @ -111,32 +109,24 @@ export const RegistrationTokenCreate = props => ( | |||
|   </Create> | ||||
| ); | ||||
| 
 | ||||
| export const RegistrationTokenEdit = props => ( | ||||
|   <Edit {...props}> | ||||
|     <SimpleForm> | ||||
|       <TextInput source="token" disabled /> | ||||
|       <NumberInput source="pending" disabled /> | ||||
|       <NumberInput source="completed" disabled /> | ||||
|       <NumberInput | ||||
|         source="uses_allowed" | ||||
|         validate={validateUsesAllowed} | ||||
|         step={1} | ||||
|       /> | ||||
|       <DateTimeInput | ||||
|         source="expiry_time" | ||||
|         parse={dateParser} | ||||
|         format={dateFormatter} | ||||
|       /> | ||||
|     </SimpleForm> | ||||
|   </Edit> | ||||
| ); | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "registration_tokens", | ||||
|   icon: RegistrationTokenIcon, | ||||
|   list: RegistrationTokenList, | ||||
|   edit: RegistrationTokenEdit, | ||||
|   create: RegistrationTokenCreate, | ||||
| export const RegistrationTokenEdit = props => { | ||||
|   return ( | ||||
|     <Edit {...props}> | ||||
|       <SimpleForm> | ||||
|         <TextInput source="token" disabled /> | ||||
|         <NumberInput source="pending" disabled /> | ||||
|         <NumberInput source="completed" disabled /> | ||||
|         <NumberInput | ||||
|           source="uses_allowed" | ||||
|           validate={validateUsesAllowed} | ||||
|           step={1} | ||||
|         /> | ||||
|         <DateTimeInput | ||||
|           source="expiry_time" | ||||
|           parse={dateParser} | ||||
|           format={dateFormatter} | ||||
|         /> | ||||
|       </SimpleForm> | ||||
|     </Edit> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
							
								
								
									
										260
									
								
								src/components/RoomDirectory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								src/components/RoomDirectory.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,260 @@ | |||
| import React, { Fragment } from "react"; | ||||
| import { Avatar, Chip } from "@mui/material"; | ||||
| import { connect } from "react-redux"; | ||||
| import FolderSharedIcon from "@mui/icons-material/FolderShared"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| import { | ||||
|   BooleanField, | ||||
|   BulkDeleteButton, | ||||
|   Button, | ||||
|   Datagrid, | ||||
|   DeleteButton, | ||||
|   Filter, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|   TextField, | ||||
|   useCreate, | ||||
|   useMutation, | ||||
|   useNotify, | ||||
|   useTranslate, | ||||
|   useRecordContext, | ||||
|   useRefresh, | ||||
|   useUnselectAll, | ||||
| } from "react-admin"; | ||||
| 
 | ||||
| const useStyles = makeStyles({ | ||||
|   small: { | ||||
|     height: "40px", | ||||
|     width: "40px", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const RoomDirectoryPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} /> | ||||
| ); | ||||
| 
 | ||||
| export const RoomDirectoryDeleteButton = props => { | ||||
|   const translate = useTranslate(); | ||||
| 
 | ||||
|   return ( | ||||
|     <DeleteButton | ||||
|       {...props} | ||||
|       label="resources.room_directory.action.erase" | ||||
|       redirect={false} | ||||
|       mutationMode="pessimistic" | ||||
|       confirmTitle={translate("resources.room_directory.action.title", { | ||||
|         smart_count: 1, | ||||
|       })} | ||||
|       confirmContent={translate("resources.room_directory.action.content", { | ||||
|         smart_count: 1, | ||||
|       })} | ||||
|       resource="room_directory" | ||||
|       icon={<FolderSharedIcon />} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RoomDirectoryBulkDeleteButton = props => ( | ||||
|   <BulkDeleteButton | ||||
|     {...props} | ||||
|     label="resources.room_directory.action.erase" | ||||
|     mutationMode="pessimistic" | ||||
|     confirmTitle="resources.room_directory.action.title" | ||||
|     confirmContent="resources.room_directory.action.content" | ||||
|     resource="room_directory" | ||||
|     icon={<FolderSharedIcon />} | ||||
|   /> | ||||
| ); | ||||
| 
 | ||||
| export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => { | ||||
|   const notify = useNotify(); | ||||
|   const refresh = useRefresh(); | ||||
|   const unselectAll = useUnselectAll(); | ||||
|   const [createMany, { loading }] = useMutation(); | ||||
| 
 | ||||
|   const handleSend = values => { | ||||
|     createMany( | ||||
|       { | ||||
|         type: "createMany", | ||||
|         resource: "room_directory", | ||||
|         payload: { ids: selectedIds, data: {} }, | ||||
|       }, | ||||
|       { | ||||
|         onSuccess: ({ data }) => { | ||||
|           notify("resources.room_directory.action.send_success"); | ||||
|           unselectAll("rooms"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onFailure: error => | ||||
|           notify("resources.room_directory.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|       } | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       label="resources.room_directory.action.create" | ||||
|       onClick={handleSend} | ||||
|       disabled={loading} | ||||
|     > | ||||
|       <FolderSharedIcon /> | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RoomDirectorySaveButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const notify = useNotify(); | ||||
|   const refresh = useRefresh(); | ||||
|   const [create, { loading }] = useCreate("room_directory"); | ||||
| 
 | ||||
|   const handleSend = values => { | ||||
|     create( | ||||
|       { | ||||
|         payload: { data: { id: record.id } }, | ||||
|       }, | ||||
|       { | ||||
|         onSuccess: ({ data }) => { | ||||
|           notify("resources.room_directory.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onFailure: error => | ||||
|           notify("resources.room_directory.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|       } | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       label="resources.room_directory.action.create" | ||||
|       onClick={handleSend} | ||||
|       disabled={loading} | ||||
|     > | ||||
|       <FolderSharedIcon /> | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomDirectoryBulkActionButtons = props => ( | ||||
|   <Fragment> | ||||
|     <RoomDirectoryBulkDeleteButton {...props} /> | ||||
|   </Fragment> | ||||
| ); | ||||
| 
 | ||||
| const AvatarField = ({ source, className, record = {} }) => ( | ||||
|   <Avatar src={record[source]} className={className} /> | ||||
| ); | ||||
| 
 | ||||
| const RoomDirectoryFilter = ({ ...props }) => { | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|     <Filter {...props}> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.room_id")} | ||||
|         source="room_id" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.topic")} | ||||
|         source="topic" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.canonical_alias")} | ||||
|         source="canonical_alias" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|     </Filter> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const FilterableRoomDirectoryList = ({ | ||||
|   roomDirectoryFilters, | ||||
|   dispatch, | ||||
|   ...props | ||||
| }) => { | ||||
|   const classes = useStyles(); | ||||
|   const filter = roomDirectoryFilters; | ||||
|   const roomIdFilter = filter && filter.room_id ? true : false; | ||||
|   const topicFilter = filter && filter.topic ? true : false; | ||||
|   const canonicalAliasFilter = filter && filter.canonical_alias ? true : false; | ||||
| 
 | ||||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       pagination={<RoomDirectoryPagination />} | ||||
|       bulkActionButtons={<RoomDirectoryBulkActionButtons />} | ||||
|       filters={<RoomDirectoryFilter />} | ||||
|       perPage={100} | ||||
|     > | ||||
|       <Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}> | ||||
|         <AvatarField | ||||
|           source="avatar_src" | ||||
|           sortable={false} | ||||
|           className={classes.small} | ||||
|           label="resources.rooms.fields.avatar" | ||||
|         /> | ||||
|         <TextField | ||||
|           source="name" | ||||
|           sortable={false} | ||||
|           label="resources.rooms.fields.name" | ||||
|         /> | ||||
|         {roomIdFilter && ( | ||||
|           <TextField | ||||
|             source="room_id" | ||||
|             sortable={false} | ||||
|             label="resources.rooms.fields.room_id" | ||||
|           /> | ||||
|         )} | ||||
|         {canonicalAliasFilter && ( | ||||
|           <TextField | ||||
|             source="canonical_alias" | ||||
|             sortable={false} | ||||
|             label="resources.rooms.fields.canonical_alias" | ||||
|           /> | ||||
|         )} | ||||
|         {topicFilter && ( | ||||
|           <TextField | ||||
|             source="topic" | ||||
|             sortable={false} | ||||
|             label="resources.rooms.fields.topic" | ||||
|           /> | ||||
|         )} | ||||
|         <NumberField | ||||
|           source="num_joined_members" | ||||
|           sortable={false} | ||||
|           label="resources.rooms.fields.joined_members" | ||||
|         /> | ||||
|         <BooleanField | ||||
|           source="world_readable" | ||||
|           sortable={false} | ||||
|           label="resources.room_directory.fields.world_readable" | ||||
|         /> | ||||
|         <BooleanField | ||||
|           source="guest_can_join" | ||||
|           sortable={false} | ||||
|           label="resources.room_directory.fields.guest_can_join" | ||||
|         /> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| function mapStateToProps(state) { | ||||
|   return { | ||||
|     roomDirectoryFilters: | ||||
|       state.admin.resources.room_directory.list.params.displayedFilters, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const RoomDirectoryList = connect(mapStateToProps)( | ||||
|   FilterableRoomDirectoryList | ||||
| ); | ||||
|  | @ -1,206 +0,0 @@ | |||
| import React from "react"; | ||||
| import { | ||||
|   BooleanField, | ||||
|   BulkDeleteButton, | ||||
|   Button, | ||||
|   DatagridConfigurable, | ||||
|   ExportButton, | ||||
|   DeleteButton, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|   SelectColumnsButton, | ||||
|   TextField, | ||||
|   TopToolbar, | ||||
|   useCreate, | ||||
|   useDataProvider, | ||||
|   useListContext, | ||||
|   useNotify, | ||||
|   useTranslate, | ||||
|   useRecordContext, | ||||
|   useRefresh, | ||||
|   useUnselectAll, | ||||
| } from "react-admin"; | ||||
| import { useMutation } from "react-query"; | ||||
| import RoomDirectoryIcon from "@mui/icons-material/FolderShared"; | ||||
| import AvatarField from "./AvatarField"; | ||||
| 
 | ||||
| const RoomDirectoryPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} /> | ||||
| ); | ||||
| 
 | ||||
| export const RoomDirectoryUnpublishButton = props => { | ||||
|   const translate = useTranslate(); | ||||
| 
 | ||||
|   return ( | ||||
|     <DeleteButton | ||||
|       {...props} | ||||
|       label="resources.room_directory.action.erase" | ||||
|       redirect={false} | ||||
|       mutationMode="pessimistic" | ||||
|       confirmTitle={translate("resources.room_directory.action.title", { | ||||
|         smart_count: 1, | ||||
|       })} | ||||
|       confirmContent={translate("resources.room_directory.action.content", { | ||||
|         smart_count: 1, | ||||
|       })} | ||||
|       resource="room_directory" | ||||
|       icon={<RoomDirectoryIcon />} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RoomDirectoryBulkUnpublishButton = props => ( | ||||
|   <BulkDeleteButton | ||||
|     {...props} | ||||
|     label="resources.room_directory.action.erase" | ||||
|     mutationMode="pessimistic" | ||||
|     confirmTitle="resources.room_directory.action.title" | ||||
|     confirmContent="resources.room_directory.action.content" | ||||
|     resource="room_directory" | ||||
|     icon={<RoomDirectoryIcon />} | ||||
|   /> | ||||
| ); | ||||
| 
 | ||||
| export const RoomDirectoryBulkPublishButton = props => { | ||||
|   const { selectedIds } = useListContext(); | ||||
|   const notify = useNotify(); | ||||
|   const refresh = useRefresh(); | ||||
|   const unselectAllRooms = useUnselectAll("rooms"); | ||||
|   const dataProvider = useDataProvider(); | ||||
|   const { mutate, isLoading } = useMutation( | ||||
|     () => | ||||
|       dataProvider.createMany("room_directory", { | ||||
|         ids: selectedIds, | ||||
|         data: {}, | ||||
|       }), | ||||
|     { | ||||
|       onSuccess: () => { | ||||
|         notify("resources.room_directory.action.send_success"); | ||||
|         unselectAllRooms(); | ||||
|         refresh(); | ||||
|       }, | ||||
|       onError: () => | ||||
|         notify("resources.room_directory.action.send_failure", { | ||||
|           type: "error", | ||||
|         }), | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       {...props} | ||||
|       label="resources.room_directory.action.create" | ||||
|       onClick={mutate} | ||||
|       disabled={isLoading} | ||||
|     > | ||||
|       <RoomDirectoryIcon /> | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RoomDirectoryPublishButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const notify = useNotify(); | ||||
|   const refresh = useRefresh(); | ||||
|   const [create, { isLoading }] = useCreate(); | ||||
| 
 | ||||
|   const handleSend = () => { | ||||
|     create( | ||||
|       "room_directory", | ||||
|       { data: { id: record.id } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.room_directory.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => | ||||
|           notify("resources.room_directory.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|       } | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       {...props} | ||||
|       label="resources.room_directory.action.create" | ||||
|       onClick={handleSend} | ||||
|       disabled={isLoading} | ||||
|     > | ||||
|       <RoomDirectoryIcon /> | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomDirectoryListActions = () => ( | ||||
|   <TopToolbar> | ||||
|     <SelectColumnsButton /> | ||||
|     <ExportButton /> | ||||
|   </TopToolbar> | ||||
| ); | ||||
| 
 | ||||
| export const RoomDirectoryList = () => ( | ||||
|   <List | ||||
|     pagination={<RoomDirectoryPagination />} | ||||
|     perPage={100} | ||||
|     actions={<RoomDirectoryListActions />} | ||||
|   > | ||||
|     <DatagridConfigurable | ||||
|       rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"} | ||||
|       bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} | ||||
|       omit={["room_id", "canonical_alias", "topic"]} | ||||
|     > | ||||
|       <AvatarField | ||||
|         source="avatar_src" | ||||
|         sortable={false} | ||||
|         sx={{ height: "40px", width: "40px" }} | ||||
|         label="resources.rooms.fields.avatar" | ||||
|       /> | ||||
|       <TextField | ||||
|         source="name" | ||||
|         sortable={false} | ||||
|         label="resources.rooms.fields.name" | ||||
|       /> | ||||
|       <TextField | ||||
|         source="room_id" | ||||
|         sortable={false} | ||||
|         label="resources.rooms.fields.room_id" | ||||
|       /> | ||||
|       <TextField | ||||
|         source="canonical_alias" | ||||
|         sortable={false} | ||||
|         label="resources.rooms.fields.canonical_alias" | ||||
|       /> | ||||
|       <TextField | ||||
|         source="topic" | ||||
|         sortable={false} | ||||
|         label="resources.rooms.fields.topic" | ||||
|       /> | ||||
|       <NumberField | ||||
|         source="num_joined_members" | ||||
|         sortable={false} | ||||
|         label="resources.rooms.fields.joined_members" | ||||
|       /> | ||||
|       <BooleanField | ||||
|         source="world_readable" | ||||
|         sortable={false} | ||||
|         label="resources.room_directory.fields.world_readable" | ||||
|       /> | ||||
|       <BooleanField | ||||
|         source="guest_can_join" | ||||
|         sortable={false} | ||||
|         label="resources.room_directory.fields.guest_can_join" | ||||
|       /> | ||||
|     </DatagridConfigurable> | ||||
|   </List> | ||||
| ); | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "room_directory", | ||||
|   icon: RoomDirectoryIcon, | ||||
|   list: RoomDirectoryList, | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
|  | @ -1,4 +1,4 @@ | |||
| import React, { useState } from "react"; | ||||
| import React, { Fragment, useState } from "react"; | ||||
| import { | ||||
|   Button, | ||||
|   SaveButton, | ||||
|  | @ -7,14 +7,12 @@ import { | |||
|   Toolbar, | ||||
|   required, | ||||
|   useCreate, | ||||
|   useDataProvider, | ||||
|   useListContext, | ||||
|   useMutation, | ||||
|   useNotify, | ||||
|   useRecordContext, | ||||
|   useTranslate, | ||||
|   useUnselectAll, | ||||
| } from "react-admin"; | ||||
| import { useMutation } from "react-query"; | ||||
| import MessageIcon from "@mui/icons-material/Message"; | ||||
| import IconCancel from "@mui/icons-material/Cancel"; | ||||
| import { | ||||
|  | @ -24,7 +22,7 @@ import { | |||
|   DialogTitle, | ||||
| } from "@mui/material"; | ||||
| 
 | ||||
| const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => { | ||||
| const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => { | ||||
|   const translate = useTranslate(); | ||||
| 
 | ||||
|   const ServerNoticeToolbar = props => ( | ||||
|  | @ -48,7 +46,12 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => { | |||
|         <DialogContentText> | ||||
|           {translate("resources.servernotices.helper.send")} | ||||
|         </DialogContentText> | ||||
|         <SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}> | ||||
|         <SimpleForm | ||||
|           toolbar={<ServerNoticeToolbar />} | ||||
|           submitOnEnter={false} | ||||
|           redirect={false} | ||||
|           save={onSend} | ||||
|         > | ||||
|           <TextInput | ||||
|             source="body" | ||||
|             label="resources.servernotices.fields.body" | ||||
|  | @ -64,25 +67,24 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const ServerNoticeButton = () => { | ||||
| export const ServerNoticeButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const notify = useNotify(); | ||||
|   const [create, { isloading }] = useCreate(); | ||||
|   const [create, { loading }] = useCreate("servernotices"); | ||||
| 
 | ||||
|   const handleDialogOpen = () => setOpen(true); | ||||
|   const handleDialogClose = () => setOpen(false); | ||||
| 
 | ||||
|   const handleSend = values => { | ||||
|     create( | ||||
|       "servernotices", | ||||
|       { data: { id: record.id, ...values } }, | ||||
|       { payload: { data: { id: record.id, ...values } } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.servernotices.action.send_success"); | ||||
|           handleDialogClose(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.servernotices.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -91,65 +93,67 @@ export const ServerNoticeButton = () => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <Fragment> | ||||
|       <Button | ||||
|         label="resources.servernotices.send" | ||||
|         onClick={handleDialogOpen} | ||||
|         disabled={isloading} | ||||
|         disabled={loading} | ||||
|       > | ||||
|         <MessageIcon /> | ||||
|       </Button> | ||||
|       <ServerNoticeDialog | ||||
|         open={open} | ||||
|         onClose={handleDialogClose} | ||||
|         onSubmit={handleSend} | ||||
|         onSend={handleSend} | ||||
|       /> | ||||
|     </> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const ServerNoticeBulkButton = () => { | ||||
|   const { selectedIds } = useListContext(); | ||||
| export const ServerNoticeBulkButton = ({ selectedIds }) => { | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const openDialog = () => setOpen(true); | ||||
|   const closeDialog = () => setOpen(false); | ||||
|   const notify = useNotify(); | ||||
|   const unselectAllUsers = useUnselectAll("users"); | ||||
|   const dataProvider = useDataProvider(); | ||||
|   const unselectAll = useUnselectAll(); | ||||
|   const [createMany, { loading }] = useMutation(); | ||||
| 
 | ||||
|   const { mutate: sendNotices, isLoading } = useMutation( | ||||
|     data => | ||||
|       dataProvider.createMany("servernotices", { | ||||
|         ids: selectedIds, | ||||
|         data: data, | ||||
|       }), | ||||
|     { | ||||
|       onSuccess: () => { | ||||
|         notify("resources.servernotices.action.send_success"); | ||||
|         unselectAllUsers(); | ||||
|         closeDialog(); | ||||
|   const handleDialogOpen = () => setOpen(true); | ||||
|   const handleDialogClose = () => setOpen(false); | ||||
| 
 | ||||
|   const handleSend = values => { | ||||
|     createMany( | ||||
|       { | ||||
|         type: "createMany", | ||||
|         resource: "servernotices", | ||||
|         payload: { ids: selectedIds, data: values }, | ||||
|       }, | ||||
|       onError: () => | ||||
|         notify("resources.servernotices.action.send_failure", { | ||||
|           type: "error", | ||||
|         }), | ||||
|     } | ||||
|   ); | ||||
|       { | ||||
|         onSuccess: ({ data }) => { | ||||
|           notify("resources.servernotices.action.send_success"); | ||||
|           unselectAll("users"); | ||||
|           handleDialogClose(); | ||||
|         }, | ||||
|         onFailure: error => | ||||
|           notify("resources.servernotices.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|       } | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <Fragment> | ||||
|       <Button | ||||
|         label="resources.servernotices.send" | ||||
|         onClick={openDialog} | ||||
|         disabled={isLoading} | ||||
|         onClick={handleDialogOpen} | ||||
|         disabled={loading} | ||||
|       > | ||||
|         <MessageIcon /> | ||||
|       </Button> | ||||
|       <ServerNoticeDialog | ||||
|         open={open} | ||||
|         onClose={closeDialog} | ||||
|         onSubmit={sendNotices} | ||||
|         onClose={handleDialogClose} | ||||
|         onSend={handleSend} | ||||
|       /> | ||||
|     </> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
|  | @ -3,6 +3,7 @@ import { | |||
|   Button, | ||||
|   Datagrid, | ||||
|   DateField, | ||||
|   Filter, | ||||
|   List, | ||||
|   Pagination, | ||||
|   ReferenceField, | ||||
|  | @ -20,12 +21,11 @@ import { | |||
|   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"; | ||||
| 
 | ||||
| const DestinationPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| const DestinationPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| const date_format = { | ||||
|  | @ -37,17 +37,23 @@ const date_format = { | |||
|   second: "2-digit", | ||||
| }; | ||||
| 
 | ||||
| const destinationRowSx = (record, _index) => ({ | ||||
| const destinationRowStyle = (record, index) => ({ | ||||
|   backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white", | ||||
| }); | ||||
| 
 | ||||
| const destinationFilters = [<SearchInput source="destination" alwaysOn />]; | ||||
| const DestinationFilter = ({ ...props }) => { | ||||
|   return ( | ||||
|     <Filter {...props}> | ||||
|       <SearchInput source="destination" alwaysOn /> | ||||
|     </Filter> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const DestinationReconnectButton = () => { | ||||
| export const DestinationReconnectButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const refresh = useRefresh(); | ||||
|   const notify = useNotify(); | ||||
|   const [handleReconnect, { isLoading }] = useDelete(); | ||||
|   const [handleReconnect, { isLoading }] = useDelete("destinations"); | ||||
| 
 | ||||
|   // Reconnect is not required if no error has occurred. (`failure_ts`)
 | ||||
|   if (!record || !record.failure_ts) return null; | ||||
|  | @ -57,8 +63,7 @@ export const DestinationReconnectButton = () => { | |||
|     e.stopPropagation(); | ||||
| 
 | ||||
|     handleReconnect( | ||||
|       "destinations", | ||||
|       { id: record.id }, | ||||
|       { payload: { id: record.id } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("ra.notification.updated", { | ||||
|  | @ -66,7 +71,7 @@ export const DestinationReconnectButton = () => { | |||
|           }); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => { | ||||
|         onFailure: () => { | ||||
|           notify("ra.message.error", { type: "error" }); | ||||
|         }, | ||||
|       } | ||||
|  | @ -84,13 +89,13 @@ export const DestinationReconnectButton = () => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const DestinationShowActions = () => ( | ||||
| const DestinationShowActions = props => ( | ||||
|   <TopToolbar> | ||||
|     <DestinationReconnectButton /> | ||||
|   </TopToolbar> | ||||
| ); | ||||
| 
 | ||||
| const DestinationTitle = () => { | ||||
| const DestinationTitle = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|  | @ -104,14 +109,14 @@ export const DestinationList = props => { | |||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       filters={destinationFilters} | ||||
|       filters={<DestinationFilter />} | ||||
|       pagination={<DestinationPagination />} | ||||
|       sort={{ field: "destination", order: "ASC" }} | ||||
|       bulkActionButtons={false} | ||||
|     > | ||||
|       <Datagrid | ||||
|         rowSx={destinationRowSx} | ||||
|         rowClick={(id, _resource, _record) => `${id}/show/rooms`} | ||||
|         bulkActionButtons={false} | ||||
|         rowStyle={destinationRowStyle} | ||||
|         rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`} | ||||
|       > | ||||
|         <TextField source="destination" /> | ||||
|         <DateField source="failure_ts" showTime options={date_format} /> | ||||
|  | @ -155,7 +160,7 @@ export const DestinationShow = props => { | |||
|           > | ||||
|             <Datagrid | ||||
|               style={{ width: "100%" }} | ||||
|               rowClick={(id, resource, record) => `/rooms/${id}/show`} | ||||
|               rowClick={(id, basePath, record) => `/rooms/${id}/show`} | ||||
|             > | ||||
|               <TextField | ||||
|                 source="room_id" | ||||
|  | @ -178,12 +183,3 @@ export const DestinationShow = props => { | |||
|     </Show> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "destinations", | ||||
|   icon: DestinationsIcon, | ||||
|   list: DestinationList, | ||||
|   show: DestinationShow, | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
							
								
								
									
										84
									
								
								src/components/devices.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/components/devices.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| import React, { Fragment, useState } from "react"; | ||||
| import { | ||||
|   Button, | ||||
|   useDelete, | ||||
|   useNotify, | ||||
|   Confirm, | ||||
|   useRecordContext, | ||||
|   useRefresh, | ||||
| } from "react-admin"; | ||||
| import ActionDelete from "@mui/icons-material/Delete"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| import { alpha } from "@mui/material/styles"; | ||||
| import classnames from "classnames"; | ||||
| 
 | ||||
| const useStyles = makeStyles( | ||||
|   theme => ({ | ||||
|     deleteButton: { | ||||
|       color: theme.palette.error.main, | ||||
|       "&:hover": { | ||||
|         backgroundColor: alpha(theme.palette.error.main, 0.12), | ||||
|         // Reset on mouse devices
 | ||||
|         "@media (hover: none)": { | ||||
|           backgroundColor: "transparent", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }), | ||||
|   { name: "RaDeleteDeviceButton" } | ||||
| ); | ||||
| 
 | ||||
| export const DeviceRemoveButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const classes = useStyles(props); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const refresh = useRefresh(); | ||||
|   const notify = useNotify(); | ||||
| 
 | ||||
|   const [removeDevice, { isLoading }] = useDelete("devices"); | ||||
| 
 | ||||
|   if (!record) return null; | ||||
| 
 | ||||
|   const handleClick = () => setOpen(true); | ||||
|   const handleDialogClose = () => setOpen(false); | ||||
| 
 | ||||
|   const handleConfirm = () => { | ||||
|     removeDevice( | ||||
|       { payload: { id: record.id, user_id: record.user_id } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.devices.action.erase.success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onFailure: () => { | ||||
|           notify("resources.devices.action.erase.failure", { type: "error" }); | ||||
|         }, | ||||
|       } | ||||
|     ); | ||||
|     setOpen(false); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Fragment> | ||||
|       <Button | ||||
|         label="ra.action.remove" | ||||
|         onClick={handleClick} | ||||
|         className={classnames("ra-delete-button", classes.deleteButton)} | ||||
|       > | ||||
|         <ActionDelete /> | ||||
|       </Button> | ||||
|       <Confirm | ||||
|         isOpen={open} | ||||
|         loading={isLoading} | ||||
|         onConfirm={handleConfirm} | ||||
|         onClose={handleDialogClose} | ||||
|         title="resources.devices.action.erase.title" | ||||
|         content="resources.devices.action.erase.content" | ||||
|         translateOptions={{ | ||||
|           id: record.id, | ||||
|           name: record.display_name ? record.display_name : record.id, | ||||
|         }} | ||||
|       /> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
|  | @ -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, | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,4 +1,7 @@ | |||
| import React, { useState } from "react"; | ||||
| import React, { Fragment, useState } from "react"; | ||||
| import classnames from "classnames"; | ||||
| import { alpha } from "@mui/material/styles"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| import { | ||||
|   BooleanInput, | ||||
|   Button, | ||||
|  | @ -27,9 +30,24 @@ import { | |||
| import IconCancel from "@mui/icons-material/Cancel"; | ||||
| import LockIcon from "@mui/icons-material/Lock"; | ||||
| import LockOpenIcon from "@mui/icons-material/LockOpen"; | ||||
| import { alpha, useTheme } from "@mui/material/styles"; | ||||
| 
 | ||||
| const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | ||||
| const useStyles = makeStyles( | ||||
|   theme => ({ | ||||
|     deleteButton: { | ||||
|       color: theme.palette.error.main, | ||||
|       "&:hover": { | ||||
|         backgroundColor: alpha(theme.palette.error.main, 0.12), | ||||
|         // Reset on mouse devices
 | ||||
|         "@media (hover: none)": { | ||||
|           backgroundColor: "transparent", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }), | ||||
|   { name: "RaDeleteDeviceButton" } | ||||
| ); | ||||
| 
 | ||||
| const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => { | ||||
|   const translate = useTranslate(); | ||||
| 
 | ||||
|   const dateParser = v => { | ||||
|  | @ -38,17 +56,19 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | |||
|     return d.getTime(); | ||||
|   }; | ||||
| 
 | ||||
|   const DeleteMediaToolbar = props => ( | ||||
|     <Toolbar {...props}> | ||||
|       <SaveButton | ||||
|         label="resources.delete_media.action.send" | ||||
|         icon={<DeleteSweepIcon />} | ||||
|       /> | ||||
|       <Button label="ra.action.cancel" onClick={onClose}> | ||||
|         <IconCancel /> | ||||
|       </Button> | ||||
|     </Toolbar> | ||||
|   ); | ||||
|   const DeleteMediaToolbar = props => { | ||||
|     return ( | ||||
|       <Toolbar {...props}> | ||||
|         <SaveButton | ||||
|           label="resources.delete_media.action.send" | ||||
|           icon={<DeleteSweepIcon />} | ||||
|         /> | ||||
|         <Button label="ra.action.cancel" onClick={onClose}> | ||||
|           <IconCancel /> | ||||
|         </Button> | ||||
|       </Toolbar> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog open={open} onClose={onClose} loading={loading}> | ||||
|  | @ -59,7 +79,12 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | |||
|         <DialogContentText> | ||||
|           {translate("resources.delete_media.helper.send")} | ||||
|         </DialogContentText> | ||||
|         <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}> | ||||
|         <SimpleForm | ||||
|           toolbar={<DeleteMediaToolbar />} | ||||
|           submitOnEnter={false} | ||||
|           redirect={false} | ||||
|           save={onSend} | ||||
|         > | ||||
|           <DateTimeInput | ||||
|             fullWidth | ||||
|             source="before_ts" | ||||
|  | @ -88,25 +113,23 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | |||
| }; | ||||
| 
 | ||||
| export const DeleteMediaButton = props => { | ||||
|   const theme = useTheme(); | ||||
|   const classes = useStyles(props); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const notify = useNotify(); | ||||
|   const [deleteOne, { isLoading }] = useDelete(); | ||||
|   const [deleteOne, { loading }] = useDelete("delete_media"); | ||||
| 
 | ||||
|   const openDialog = () => setOpen(true); | ||||
|   const closeDialog = () => setOpen(false); | ||||
|   const handleDialogOpen = () => setOpen(true); | ||||
|   const handleDialogClose = () => setOpen(false); | ||||
| 
 | ||||
|   const deleteMedia = values => { | ||||
|   const handleSend = values => { | ||||
|     deleteOne( | ||||
|       "delete_media", | ||||
|       // needs meta.before_ts, meta.size_gt and meta.keep_profiles
 | ||||
|       { meta: values }, | ||||
|       { payload: { ...values } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.delete_media.action.send_success"); | ||||
|           closeDialog(); | ||||
|           handleDialogClose(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.delete_media.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -115,54 +138,43 @@ export const DeleteMediaButton = props => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <Fragment> | ||||
|       <Button | ||||
|         {...props} | ||||
|         label="resources.delete_media.action.send" | ||||
|         onClick={openDialog} | ||||
|         disabled={isLoading} | ||||
|         sx={{ | ||||
|           color: theme.palette.error.main, | ||||
|           "&:hover": { | ||||
|             backgroundColor: alpha(theme.palette.error.main, 0.12), | ||||
|             // Reset on mouse devices
 | ||||
|             "@media (hover: none)": { | ||||
|               backgroundColor: "transparent", | ||||
|             }, | ||||
|           }, | ||||
|         }} | ||||
|         onClick={handleDialogOpen} | ||||
|         disabled={loading} | ||||
|         className={classnames("ra-delete-button", classes.deleteButton)} | ||||
|       > | ||||
|         <DeleteSweepIcon /> | ||||
|       </Button> | ||||
|       <DeleteMediaDialog | ||||
|         open={open} | ||||
|         onClose={closeDialog} | ||||
|         onSubmit={deleteMedia} | ||||
|         onClose={handleDialogClose} | ||||
|         onSend={handleSend} | ||||
|       /> | ||||
|     </> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const ProtectMediaButton = () => { | ||||
| export const ProtectMediaButton = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const translate = useTranslate(); | ||||
|   const refresh = useRefresh(); | ||||
|   const notify = useNotify(); | ||||
|   const [create, { isLoading }] = useCreate(); | ||||
|   const [deleteOne] = useDelete(); | ||||
|   const [create, { loading }] = useCreate("protect_media"); | ||||
|   const [deleteOne] = useDelete("protect_media"); | ||||
| 
 | ||||
|   if (!record) return null; | ||||
| 
 | ||||
|   const handleProtect = () => { | ||||
|     create( | ||||
|       "protect_media", | ||||
|       { data: record }, | ||||
|       { payload: { data: record } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.protect_media.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.protect_media.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -172,14 +184,13 @@ export const ProtectMediaButton = () => { | |||
| 
 | ||||
|   const handleUnprotect = () => { | ||||
|     deleteOne( | ||||
|       "protect_media", | ||||
|       { id: record.id }, | ||||
|       { payload: { ...record } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.protect_media.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.protect_media.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -192,7 +203,7 @@ export const ProtectMediaButton = () => { | |||
|     Wrapping Tooltip with <div> | ||||
|     https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
 | ||||
|     */ | ||||
|     <> | ||||
|     <Fragment> | ||||
|       {record.quarantined_by && ( | ||||
|         <Tooltip | ||||
|           title={translate("resources.protect_media.action.none", { | ||||
|  | @ -218,7 +229,7 @@ export const ProtectMediaButton = () => { | |||
|           arrow | ||||
|         > | ||||
|           <div> | ||||
|             <Button onClick={handleUnprotect} disabled={isLoading}> | ||||
|             <Button onClick={handleUnprotect} disabled={loading}> | ||||
|               <LockIcon /> | ||||
|             </Button> | ||||
|           </div> | ||||
|  | @ -231,13 +242,13 @@ export const ProtectMediaButton = () => { | |||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             <Button onClick={handleProtect} disabled={isLoading}> | ||||
|             <Button onClick={handleProtect} disabled={loading}> | ||||
|               <LockOpenIcon /> | ||||
|             </Button> | ||||
|           </div> | ||||
|         </Tooltip> | ||||
|       )} | ||||
|     </> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
|  | @ -246,21 +257,20 @@ export const QuarantineMediaButton = props => { | |||
|   const translate = useTranslate(); | ||||
|   const refresh = useRefresh(); | ||||
|   const notify = useNotify(); | ||||
|   const [create, { isLoading }] = useCreate(); | ||||
|   const [deleteOne] = useDelete(); | ||||
|   const [create, { loading }] = useCreate("quarantine_media"); | ||||
|   const [deleteOne] = useDelete("quarantine_media"); | ||||
| 
 | ||||
|   if (!record) return null; | ||||
| 
 | ||||
|   const handleQuarantaine = () => { | ||||
|     create( | ||||
|       "quarantine_media", | ||||
|       { data: record }, | ||||
|       { payload: { data: record } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.quarantine_media.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.quarantine_media.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -270,14 +280,13 @@ export const QuarantineMediaButton = props => { | |||
| 
 | ||||
|   const handleRemoveQuarantaine = () => { | ||||
|     deleteOne( | ||||
|       "quarantine_media", | ||||
|       { id: record.id, previousData: record }, | ||||
|       { payload: { ...record } }, | ||||
|       { | ||||
|         onSuccess: () => { | ||||
|           notify("resources.quarantine_media.action.send_success"); | ||||
|           refresh(); | ||||
|         }, | ||||
|         onError: () => | ||||
|         onFailure: () => | ||||
|           notify("resources.quarantine_media.action.send_failure", { | ||||
|             type: "error", | ||||
|           }), | ||||
|  | @ -286,7 +295,7 @@ export const QuarantineMediaButton = props => { | |||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <Fragment> | ||||
|       {record.safe_from_quarantine && ( | ||||
|         <Tooltip | ||||
|           title={translate("resources.quarantine_media.action.none", { | ||||
|  | @ -294,7 +303,7 @@ export const QuarantineMediaButton = props => { | |||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             <Button {...props} disabled={true}> | ||||
|             <Button disabled={true}> | ||||
|               <ClearIcon /> | ||||
|             </Button> | ||||
|           </div> | ||||
|  | @ -307,11 +316,7 @@ export const QuarantineMediaButton = props => { | |||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             <Button | ||||
|               {...props} | ||||
|               onClick={handleRemoveQuarantaine} | ||||
|               disabled={isLoading} | ||||
|             > | ||||
|             <Button onClick={handleRemoveQuarantaine} disabled={loading}> | ||||
|               <BlockIcon color="error" /> | ||||
|             </Button> | ||||
|           </div> | ||||
|  | @ -324,12 +329,12 @@ export const QuarantineMediaButton = props => { | |||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             <Button onClick={handleQuarantaine} disabled={isLoading}> | ||||
|             <Button onClick={handleQuarantaine} disabled={loading}> | ||||
|               <BlockIcon /> | ||||
|             </Button> | ||||
|           </div> | ||||
|         </Tooltip> | ||||
|       )} | ||||
|     </> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,20 +1,18 @@ | |||
| import React from "react"; | ||||
| import React, { Fragment } from "react"; | ||||
| import { connect } from "react-redux"; | ||||
| import { | ||||
|   BooleanField, | ||||
|   BulkDeleteButton, | ||||
|   DateField, | ||||
|   Datagrid, | ||||
|   DatagridConfigurable, | ||||
|   DeleteButton, | ||||
|   ExportButton, | ||||
|   FunctionField, | ||||
|   Filter, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|   ReferenceField, | ||||
|   ReferenceManyField, | ||||
|   SearchInput, | ||||
|   SelectColumnsButton, | ||||
|   SelectField, | ||||
|   Show, | ||||
|   Tab, | ||||
|  | @ -24,8 +22,10 @@ import { | |||
|   useRecordContext, | ||||
|   useTranslate, | ||||
| } from "react-admin"; | ||||
| import { useTheme } from "@mui/material/styles"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import get from "lodash/get"; | ||||
| import PropTypes from "prop-types"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| import { Tooltip, Typography, Chip } from "@mui/material"; | ||||
| import FastForwardIcon from "@mui/icons-material/FastForward"; | ||||
| import HttpsIcon from "@mui/icons-material/Https"; | ||||
| import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; | ||||
|  | @ -34,12 +34,11 @@ import UserIcon from "@mui/icons-material/Group"; | |||
| import ViewListIcon from "@mui/icons-material/ViewList"; | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import EventIcon from "@mui/icons-material/Event"; | ||||
| import RoomIcon from "@mui/icons-material/ViewList"; | ||||
| import { | ||||
|   RoomDirectoryBulkUnpublishButton, | ||||
|   RoomDirectoryBulkPublishButton, | ||||
|   RoomDirectoryUnpublishButton, | ||||
|   RoomDirectoryPublishButton, | ||||
|   RoomDirectoryBulkDeleteButton, | ||||
|   RoomDirectoryBulkSaveButton, | ||||
|   RoomDirectoryDeleteButton, | ||||
|   RoomDirectorySaveButton, | ||||
| } from "./RoomDirectory"; | ||||
| 
 | ||||
| const date_format = { | ||||
|  | @ -51,11 +50,44 @@ const date_format = { | |||
|   second: "2-digit", | ||||
| }; | ||||
| 
 | ||||
| const RoomPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| const useStyles = makeStyles(theme => ({ | ||||
|   helper_forward_extremities: { | ||||
|     fontFamily: "Roboto, Helvetica, Arial, sans-serif", | ||||
|     margin: "0.5em", | ||||
|   }, | ||||
| })); | ||||
| 
 | ||||
| const RoomPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| const RoomTitle = () => { | ||||
| const EncryptionField = ({ source, record = {}, emptyText }) => { | ||||
|   const translate = useTranslate(); | ||||
|   const value = get(record, source); | ||||
|   let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true"; | ||||
| 
 | ||||
|   if (value === false || value === true) { | ||||
|     return ( | ||||
|       <Typography component="span" variant="body2"> | ||||
|         <Tooltip title={translate(ariaLabel, { _: ariaLabel })}> | ||||
|           {value === true ? ( | ||||
|             <HttpsIcon data-testid="true" htmlColor="limegreen" /> | ||||
|           ) : ( | ||||
|             <NoEncryptionIcon data-testid="false" color="error" /> | ||||
|           )} | ||||
|         </Tooltip> | ||||
|       </Typography> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Typography component="span" variant="body2"> | ||||
|       {emptyText} | ||||
|     </Typography> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomTitle = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const translate = useTranslate(); | ||||
|   var name = ""; | ||||
|  | @ -70,18 +102,24 @@ const RoomTitle = () => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomShowActions = () => { | ||||
|   const record = useRecordContext(); | ||||
| const RoomShowActions = ({ basePath, data, resource }) => { | ||||
|   var roomDirectoryStatus = ""; | ||||
|   if (record) { | ||||
|     roomDirectoryStatus = record.public; | ||||
|   if (data) { | ||||
|     roomDirectoryStatus = data.public; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <TopToolbar> | ||||
|       {roomDirectoryStatus === false && <RoomDirectoryPublishButton />} | ||||
|       {roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />} | ||||
|       {roomDirectoryStatus === false && ( | ||||
|         <RoomDirectorySaveButton record={data} /> | ||||
|       )} | ||||
|       {roomDirectoryStatus === true && ( | ||||
|         <RoomDirectoryDeleteButton record={data} /> | ||||
|       )} | ||||
|       <DeleteButton | ||||
|         basePath={basePath} | ||||
|         record={data} | ||||
|         resource={resource} | ||||
|         mutationMode="pessimistic" | ||||
|         confirmTitle="resources.rooms.action.erase.title" | ||||
|         confirmContent="resources.rooms.action.erase.content" | ||||
|  | @ -91,6 +129,7 @@ const RoomShowActions = () => { | |||
| }; | ||||
| 
 | ||||
| export const RoomShow = props => { | ||||
|   const classes = useStyles({ props }); | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|     <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> | ||||
|  | @ -98,7 +137,6 @@ export const RoomShow = props => { | |||
|         <Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}> | ||||
|           <TextField source="room_id" /> | ||||
|           <TextField source="name" /> | ||||
|           <TextField source="topic" /> | ||||
|           <TextField source="canonical_alias" /> | ||||
|           <ReferenceField source="creator" reference="users"> | ||||
|             <TextField source="id" /> | ||||
|  | @ -133,8 +171,7 @@ export const RoomShow = props => { | |||
|           > | ||||
|             <Datagrid | ||||
|               style={{ width: "100%" }} | ||||
|               rowClick={(id, resource, record) => "/users/" + id} | ||||
|               bulkActionButtons={false} | ||||
|               rowClick={(id, basePath, record) => "/users/" + id} | ||||
|             > | ||||
|               <TextField | ||||
|                 source="id" | ||||
|  | @ -219,7 +256,7 @@ export const RoomShow = props => { | |||
|             target="room_id" | ||||
|             addLabel={false} | ||||
|           > | ||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> | ||||
|             <Datagrid style={{ width: "100%" }}> | ||||
|               <TextField source="type" sortable={false} /> | ||||
|               <DateField | ||||
|                 source="origin_server_ts" | ||||
|  | @ -244,20 +281,15 @@ export const RoomShow = props => { | |||
|           icon={<FastForwardIcon />} | ||||
|           path="forward_extremities" | ||||
|         > | ||||
|           <Box | ||||
|             sx={{ | ||||
|               fontFamily: "Roboto, Helvetica, Arial, sans-serif", | ||||
|               margin: "0.5em", | ||||
|             }} | ||||
|           > | ||||
|           <div className={classes.helper_forward_extremities}> | ||||
|             {translate("resources.rooms.helper.forward_extremities")} | ||||
|           </Box> | ||||
|           </div> | ||||
|           <ReferenceManyField | ||||
|             reference="forward_extremities" | ||||
|             target="room_id" | ||||
|             addLabel={false} | ||||
|           > | ||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> | ||||
|             <Datagrid style={{ width: "100%" }}> | ||||
|               <TextField source="id" sortable={false} /> | ||||
|               <DateField | ||||
|                 source="received_ts" | ||||
|  | @ -275,81 +307,104 @@ export const RoomShow = props => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomBulkActionButtons = () => ( | ||||
|   <> | ||||
|     <RoomDirectoryBulkPublishButton /> | ||||
|     <RoomDirectoryBulkUnpublishButton /> | ||||
| const RoomBulkActionButtons = props => ( | ||||
|   <Fragment> | ||||
|     <RoomDirectoryBulkSaveButton {...props} /> | ||||
|     <RoomDirectoryBulkDeleteButton {...props} /> | ||||
|     <BulkDeleteButton | ||||
|       {...props} | ||||
|       confirmTitle="resources.rooms.action.erase.title" | ||||
|       confirmContent="resources.rooms.action.erase.content" | ||||
|       mutationMode="pessimistic" | ||||
|     /> | ||||
|   </> | ||||
|   </Fragment> | ||||
| ); | ||||
| 
 | ||||
| const roomFilters = [<SearchInput source="search_term" alwaysOn />]; | ||||
| const RoomFilter = ({ ...props }) => { | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|     <Filter {...props}> | ||||
|       <SearchInput source="search_term" alwaysOn /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.joined_local_members")} | ||||
|         source="joined_local_members" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.state_events")} | ||||
|         source="state_events" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.version")} | ||||
|         source="version" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|       <Chip | ||||
|         label={translate("resources.rooms.fields.federatable")} | ||||
|         source="federatable" | ||||
|         defaultValue={false} | ||||
|         style={{ marginBottom: 8 }} | ||||
|       /> | ||||
|     </Filter> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const RoomListActions = () => ( | ||||
|   <TopToolbar> | ||||
|     <SelectColumnsButton /> | ||||
|     <ExportButton /> | ||||
|   </TopToolbar> | ||||
| ); | ||||
| const RoomNameField = props => { | ||||
|   const { source } = props; | ||||
|   const record = useRecordContext(); | ||||
|   return ( | ||||
|     <span>{record[source] || record["canonical_alias"] || record["id"]}</span> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const RoomList = props => { | ||||
|   const theme = useTheme(); | ||||
| RoomNameField.propTypes = { | ||||
|   label: PropTypes.string, | ||||
|   record: PropTypes.object, | ||||
|   source: PropTypes.string.isRequired, | ||||
| }; | ||||
| 
 | ||||
| const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => { | ||||
|   const filter = roomFilters; | ||||
|   const localMembersFilter = | ||||
|     filter && filter.joined_local_members ? true : false; | ||||
|   const stateEventsFilter = filter && filter.state_events ? true : false; | ||||
|   const versionFilter = filter && filter.version ? true : false; | ||||
|   const federateableFilter = filter && filter.federatable ? true : false; | ||||
| 
 | ||||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       pagination={<RoomPagination />} | ||||
|       sort={{ field: "name", order: "ASC" }} | ||||
|       filters={roomFilters} | ||||
|       actions={<RoomListActions />} | ||||
|       filters={<RoomFilter />} | ||||
|       bulkActionButtons={<RoomBulkActionButtons />} | ||||
|     > | ||||
|       <DatagridConfigurable | ||||
|         rowClick="show" | ||||
|         bulkActionButtons={<RoomBulkActionButtons />} | ||||
|         omit={[ | ||||
|           "joined_local_members", | ||||
|           "state_events", | ||||
|           "version", | ||||
|           "federatable", | ||||
|         ]} | ||||
|       > | ||||
|         <BooleanField | ||||
|       <Datagrid rowClick="show"> | ||||
|         <EncryptionField | ||||
|           source="is_encrypted" | ||||
|           sortBy="encryption" | ||||
|           TrueIcon={HttpsIcon} | ||||
|           FalseIcon={NoEncryptionIcon} | ||||
|           label={<HttpsIcon />} | ||||
|           sx={{ | ||||
|             [`& [data-testid="true"]`]: { color: theme.palette.success.main }, | ||||
|             [`& [data-testid="false"]`]: { color: theme.palette.error.main }, | ||||
|           }} | ||||
|         /> | ||||
|         <FunctionField | ||||
|           source="name" | ||||
|           render={record => | ||||
|             record["name"] || record["canonical_alias"] || record["id"] | ||||
|           } | ||||
|         /> | ||||
|         <RoomNameField source="name" /> | ||||
|         <TextField source="joined_members" /> | ||||
|         <TextField source="joined_local_members" /> | ||||
|         <TextField source="state_events" /> | ||||
|         <TextField source="version" /> | ||||
|         <BooleanField source="federatable" /> | ||||
|         {localMembersFilter && <TextField source="joined_local_members" />} | ||||
|         {stateEventsFilter && <TextField source="state_events" />} | ||||
|         {versionFilter && <TextField source="version" />} | ||||
|         {federateableFilter && <BooleanField source="federatable" />} | ||||
|         <BooleanField source="public" /> | ||||
|       </DatagridConfigurable> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "rooms", | ||||
|   icon: RoomIcon, | ||||
|   list: RoomList, | ||||
|   show: RoomShow, | ||||
| }; | ||||
| function mapStateToProps(state) { | ||||
|   return { | ||||
|     roomFilters: state.admin.resources.rooms.list.params.displayedFilters, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export default resource; | ||||
| export const RoomList = connect(mapStateToProps)(FilterableRoomList); | ||||
							
								
								
									
										81
									
								
								src/components/statistics.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/components/statistics.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| import React from "react"; | ||||
| import { cloneElement } from "react"; | ||||
| import { | ||||
|   Datagrid, | ||||
|   ExportButton, | ||||
|   Filter, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|   sanitizeListRestProps, | ||||
|   SearchInput, | ||||
|   TextField, | ||||
|   TopToolbar, | ||||
|   useListContext, | ||||
| } from "react-admin"; | ||||
| import { DeleteMediaButton } from "./media"; | ||||
| 
 | ||||
| const ListActions = props => { | ||||
|   const { className, exporter, filters, maxResults, ...rest } = props; | ||||
|   const { | ||||
|     currentSort, | ||||
|     resource, | ||||
|     displayedFilters, | ||||
|     filterValues, | ||||
|     showFilter, | ||||
|     total, | ||||
|   } = useListContext(); | ||||
|   return ( | ||||
|     <TopToolbar className={className} {...sanitizeListRestProps(rest)}> | ||||
|       {filters && | ||||
|         cloneElement(filters, { | ||||
|           resource, | ||||
|           showFilter, | ||||
|           displayedFilters, | ||||
|           filterValues, | ||||
|           context: "button", | ||||
|         })} | ||||
|       <DeleteMediaButton /> | ||||
|       <ExportButton | ||||
|         disabled={total === 0} | ||||
|         resource={resource} | ||||
|         sort={currentSort} | ||||
|         filterValues={filterValues} | ||||
|         maxResults={maxResults} | ||||
|       /> | ||||
|     </TopToolbar> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const UserMediaStatsPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| const UserMediaStatsFilter = props => ( | ||||
|   <Filter {...props}> | ||||
|     <SearchInput source="search_term" alwaysOn /> | ||||
|   </Filter> | ||||
| ); | ||||
| 
 | ||||
| export const UserMediaStatsList = props => { | ||||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       actions={<ListActions />} | ||||
|       filters={<UserMediaStatsFilter />} | ||||
|       pagination={<UserMediaStatsPagination />} | ||||
|       sort={{ field: "media_length", order: "DESC" }} | ||||
|       bulkActionButtons={false} | ||||
|     > | ||||
|       <Datagrid rowClick={(id, basePath, record) => "/users/" + id + "/media"}> | ||||
|         <TextField source="user_id" label="resources.users.fields.id" /> | ||||
|         <TextField | ||||
|           source="displayname" | ||||
|           label="resources.users.fields.displayname" | ||||
|         /> | ||||
|         <NumberField source="media_count" /> | ||||
|         <NumberField source="media_length" /> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,79 +0,0 @@ | |||
| import React from "react"; | ||||
| import { cloneElement } from "react"; | ||||
| import { | ||||
|   Datagrid, | ||||
|   ExportButton, | ||||
|   List, | ||||
|   NumberField, | ||||
|   Pagination, | ||||
|   sanitizeListRestProps, | ||||
|   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(); | ||||
|   return ( | ||||
|     <TopToolbar className={className} {...sanitizeListRestProps(rest)}> | ||||
|       {filters && | ||||
|         cloneElement(filters, { | ||||
|           resource, | ||||
|           showFilter, | ||||
|           displayedFilters, | ||||
|           filterValues, | ||||
|           context: "button", | ||||
|         })} | ||||
|       <DeleteMediaButton /> | ||||
|       <ExportButton | ||||
|         disabled={total === 0} | ||||
|         resource={resource} | ||||
|         sort={sort} | ||||
|         filterValues={filterValues} | ||||
|         maxResults={maxResults} | ||||
|       /> | ||||
|     </TopToolbar> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const UserMediaStatsPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />]; | ||||
| 
 | ||||
| export const UserMediaStatsList = props => ( | ||||
|   <List | ||||
|     {...props} | ||||
|     actions={<ListActions />} | ||||
|     filters={userMediaStatsFilters} | ||||
|     pagination={<UserMediaStatsPagination />} | ||||
|     sort={{ field: "media_length", order: "DESC" }} | ||||
|   > | ||||
|     <Datagrid | ||||
|       rowClick={(id, resource, record) => "/users/" + id + "/media"} | ||||
|       bulkActionButtons={false} | ||||
|     > | ||||
|       <TextField source="user_id" label="resources.users.fields.id" /> | ||||
|       <TextField | ||||
|         source="displayname" | ||||
|         label="resources.users.fields.displayname" | ||||
|       /> | ||||
|       <NumberField source="media_count" /> | ||||
|       <NumberField source="media_length" /> | ||||
|     </Datagrid> | ||||
|   </List> | ||||
| ); | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "user_media_statistics", | ||||
|   icon: EqualizerIcon, | ||||
|   list: UserMediaStatsList, | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
|  | @ -1,4 +1,5 @@ | |||
| import React, { cloneElement } from "react"; | ||||
| import React, { cloneElement, Fragment } from "react"; | ||||
| import Avatar from "@mui/material/Avatar"; | ||||
| import AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; | ||||
| import ContactMailIcon from "@mui/icons-material/ContactMail"; | ||||
| import DevicesIcon from "@mui/icons-material/Devices"; | ||||
|  | @ -7,7 +8,6 @@ import NotificationsIcon from "@mui/icons-material/Notifications"; | |||
| import PermMediaIcon from "@mui/icons-material/PermMedia"; | ||||
| import PersonPinIcon from "@mui/icons-material/PersonPin"; | ||||
| import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; | ||||
| import UserIcon from "@mui/icons-material/Group"; | ||||
| import ViewListIcon from "@mui/icons-material/ViewList"; | ||||
| import { | ||||
|   ArrayInput, | ||||
|  | @ -18,6 +18,7 @@ import { | |||
|   Create, | ||||
|   Edit, | ||||
|   List, | ||||
|   Filter, | ||||
|   Toolbar, | ||||
|   SimpleForm, | ||||
|   SimpleFormIterator, | ||||
|  | @ -48,10 +49,28 @@ import { | |||
|   NumberField, | ||||
| } from "react-admin"; | ||||
| import { Link } from "react-router-dom"; | ||||
| import AvatarField from "./AvatarField"; | ||||
| import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; | ||||
| import { DeviceRemoveButton } from "./devices"; | ||||
| import { ProtectMediaButton, QuarantineMediaButton } from "./media"; | ||||
| import { makeStyles } from "@material-ui/core/styles"; | ||||
| 
 | ||||
| const redirect = () => { | ||||
|   return { | ||||
|     pathname: "/import_users", | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const useStyles = makeStyles({ | ||||
|   small: { | ||||
|     height: "40px", | ||||
|     width: "40px", | ||||
|   }, | ||||
|   large: { | ||||
|     height: "120px", | ||||
|     width: "120px", | ||||
|     float: "right", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const choices_medium = [ | ||||
|   { id: "email", name: "resources.users.email" }, | ||||
|  | @ -73,7 +92,7 @@ const date_format = { | |||
| }; | ||||
| 
 | ||||
| const UserListActions = ({ | ||||
|   sort, | ||||
|   currentSort, | ||||
|   className, | ||||
|   resource, | ||||
|   filters, | ||||
|  | @ -82,6 +101,7 @@ const UserListActions = ({ | |||
|   filterValues, | ||||
|   permanentFilter, | ||||
|   hasCreate, // you can hide CreateButton if hasCreate = false
 | ||||
|   basePath, | ||||
|   selectedIds, | ||||
|   onUnselectItems, | ||||
|   showFilter, | ||||
|  | @ -99,18 +119,18 @@ const UserListActions = ({ | |||
|           filterValues, | ||||
|           context: "button", | ||||
|         })} | ||||
|       <CreateButton /> | ||||
|       <CreateButton basePath={basePath} /> | ||||
|       <ExportButton | ||||
|         disabled={total === 0} | ||||
|         resource={resource} | ||||
|         sort={sort} | ||||
|         sort={currentSort} | ||||
|         filter={{ ...filterValues, ...permanentFilter }} | ||||
|         exporter={exporter} | ||||
|         maxResults={maxResults} | ||||
|       /> | ||||
|       {/* Add your custom actions */} | ||||
|       <Button component={Link} to="/import_users" label="CSV Import"> | ||||
|         <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} /> | ||||
|       <Button component={Link} to={redirect} label="CSV Import"> | ||||
|         <GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> | ||||
|       </Button> | ||||
|     </TopToolbar> | ||||
|   ); | ||||
|  | @ -121,61 +141,72 @@ UserListActions.defaultProps = { | |||
|   onUnselectItems: () => null, | ||||
| }; | ||||
| 
 | ||||
| const UserPagination = () => ( | ||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| const UserPagination = props => ( | ||||
|   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||
| ); | ||||
| 
 | ||||
| const userFilters = [ | ||||
|   <SearchInput source="name" alwaysOn />, | ||||
|   <BooleanInput source="guests" alwaysOn />, | ||||
|   <BooleanInput | ||||
|     label="resources.users.fields.show_deactivated" | ||||
|     source="deactivated" | ||||
|     alwaysOn | ||||
|   />, | ||||
| ]; | ||||
| const UserFilter = props => ( | ||||
|   <Filter {...props}> | ||||
|     <SearchInput source="name" alwaysOn /> | ||||
|     <BooleanInput source="guests" alwaysOn /> | ||||
|     <BooleanInput | ||||
|       label="resources.users.fields.show_deactivated" | ||||
|       source="deactivated" | ||||
|       alwaysOn | ||||
|     /> | ||||
|   </Filter> | ||||
| ); | ||||
| 
 | ||||
| const UserBulkActionButtons = () => ( | ||||
|   <> | ||||
|     <ServerNoticeBulkButton /> | ||||
| const UserBulkActionButtons = props => ( | ||||
|   <Fragment> | ||||
|     <ServerNoticeBulkButton {...props} /> | ||||
|     <BulkDeleteButton | ||||
|       {...props} | ||||
|       label="resources.users.action.erase" | ||||
|       confirmTitle="resources.users.helper.erase" | ||||
|       mutationMode="pessimistic" | ||||
|     /> | ||||
|   </> | ||||
|   </Fragment> | ||||
| ); | ||||
| 
 | ||||
| export const UserList = props => ( | ||||
|   <List | ||||
|     {...props} | ||||
|     filters={userFilters} | ||||
|     filterDefaultValues={{ guests: true, deactivated: false }} | ||||
|     sort={{ field: "name", order: "ASC" }} | ||||
|     actions={<UserListActions maxResults={10000} />} | ||||
|     pagination={<UserPagination />} | ||||
|   > | ||||
|     <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}> | ||||
|       <AvatarField | ||||
|         source="avatar_src" | ||||
|         sx={{ height: "40px", width: "40px" }} | ||||
|         sortBy="avatar_url" | ||||
|       /> | ||||
|       <TextField source="id" sortBy="name" /> | ||||
|       <TextField source="displayname" /> | ||||
|       <BooleanField source="is_guest" /> | ||||
|       <BooleanField source="admin" /> | ||||
|       <BooleanField source="deactivated" /> | ||||
|       <DateField | ||||
|         source="creation_ts" | ||||
|         label="resources.users.fields.creation_ts_ms" | ||||
|         showTime | ||||
|         options={date_format} | ||||
|       /> | ||||
|     </Datagrid> | ||||
|   </List> | ||||
| const AvatarField = ({ source, className, record = {} }) => ( | ||||
|   <Avatar src={record[source]} className={className} /> | ||||
| ); | ||||
| 
 | ||||
| export const UserList = props => { | ||||
|   const classes = useStyles(); | ||||
|   return ( | ||||
|     <List | ||||
|       {...props} | ||||
|       filters={<UserFilter />} | ||||
|       filterDefaultValues={{ guests: true, deactivated: false }} | ||||
|       sort={{ field: "name", order: "ASC" }} | ||||
|       actions={<UserListActions maxResults={10000} />} | ||||
|       bulkActionButtons={<UserBulkActionButtons />} | ||||
|       pagination={<UserPagination />} | ||||
|     > | ||||
|       <Datagrid rowClick="edit"> | ||||
|         <AvatarField | ||||
|           source="avatar_src" | ||||
|           className={classes.small} | ||||
|           sortBy="avatar_url" | ||||
|         /> | ||||
|         <TextField source="id" sortBy="name" /> | ||||
|         <TextField source="displayname" /> | ||||
|         <BooleanField source="is_guest" /> | ||||
|         <BooleanField source="admin" /> | ||||
|         <BooleanField source="deactivated" /> | ||||
|         <DateField | ||||
|           source="creation_ts" | ||||
|           label="resources.users.fields.creation_ts_ms" | ||||
|           showTime | ||||
|           options={date_format} | ||||
|         /> | ||||
|       </Datagrid> | ||||
|     </List> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // https://matrix.org/docs/spec/appendices#user-identifiers
 | ||||
| // here only local part of user_id
 | ||||
| // maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
 | ||||
|  | @ -231,7 +262,7 @@ export function generateRandomUser() { | |||
| 
 | ||||
| const UserEditToolbar = props => ( | ||||
|   <Toolbar {...props}> | ||||
|     <SaveButton disabled={props.pristine} /> | ||||
|     <SaveButton submitOnEnter={true} disabled={props.pristine} /> | ||||
|   </Toolbar> | ||||
| ); | ||||
| 
 | ||||
|  | @ -271,6 +302,7 @@ export const UserCreate = props => ( | |||
|         source="user_type" | ||||
|         choices={choices_type} | ||||
|         translateChoice={false} | ||||
|         allowEmpty={true} | ||||
|         resettable | ||||
|       /> | ||||
|       <BooleanInput source="admin" /> | ||||
|  | @ -298,7 +330,7 @@ export const UserCreate = props => ( | |||
|   </Create> | ||||
| ); | ||||
| 
 | ||||
| const UserTitle = () => { | ||||
| const UserTitle = props => { | ||||
|   const record = useRecordContext(); | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|  | @ -312,6 +344,7 @@ const UserTitle = () => { | |||
| }; | ||||
| 
 | ||||
| export const UserEdit = props => { | ||||
|   const classes = useStyles(); | ||||
|   const translate = useTranslate(); | ||||
|   return ( | ||||
|     <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> | ||||
|  | @ -323,7 +356,7 @@ export const UserEdit = props => { | |||
|           <AvatarField | ||||
|             source="avatar_src" | ||||
|             sortable={false} | ||||
|             sx={{ height: "120px", width: "120px", float: "right" }} | ||||
|             className={classes.large} | ||||
|           /> | ||||
|           <TextInput source="id" disabled /> | ||||
|           <TextInput source="displayname" /> | ||||
|  | @ -336,6 +369,7 @@ export const UserEdit = props => { | |||
|             source="user_type" | ||||
|             choices={choices_type} | ||||
|             translateChoice={false} | ||||
|             allowEmpty={true} | ||||
|             resettable | ||||
|           /> | ||||
|           <BooleanInput source="admin" /> | ||||
|  | @ -417,7 +451,7 @@ export const UserEdit = props => { | |||
|               source="devices[].sessions[0].connections" | ||||
|               label="resources.connections.name" | ||||
|             > | ||||
|               <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> | ||||
|               <Datagrid style={{ width: "100%" }}> | ||||
|                 <TextField source="ip" sortable={false} /> | ||||
|                 <DateField | ||||
|                   source="last_seen" | ||||
|  | @ -479,8 +513,7 @@ export const UserEdit = props => { | |||
|           > | ||||
|             <Datagrid | ||||
|               style={{ width: "100%" }} | ||||
|               rowClick={(id, resource, record) => "/rooms/" + id + "/show"} | ||||
|               bulkActionButtons={false} | ||||
|               rowClick={(id, basePath, record) => "/rooms/" + id + "/show"} | ||||
|             > | ||||
|               <TextField | ||||
|                 source="id" | ||||
|  | @ -510,7 +543,7 @@ export const UserEdit = props => { | |||
|             target="user_id" | ||||
|             addLabel={false} | ||||
|           > | ||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> | ||||
|             <Datagrid style={{ width: "100%" }}> | ||||
|               <TextField source="kind" sortable={false} /> | ||||
|               <TextField source="app_display_name" sortable={false} /> | ||||
|               <TextField source="app_id" sortable={false} /> | ||||
|  | @ -526,13 +559,3 @@ export const UserEdit = props => { | |||
|     </Edit> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const resource = { | ||||
|   name: "users", | ||||
|   icon: UserIcon, | ||||
|   list: UserList, | ||||
|   edit: UserEdit, | ||||
|   create: UserCreate, | ||||
| }; | ||||
| 
 | ||||
| export default resource; | ||||
|  | @ -188,7 +188,7 @@ const de = { | |||
|       }, | ||||
|     }, | ||||
|     reports: { | ||||
|       name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse", | ||||
|       name: "Ereignisbericht |||| Ereignisberichte", | ||||
|       fields: { | ||||
|         id: "ID", | ||||
|         received_ts: "Meldezeit", | ||||
|  | @ -210,13 +210,6 @@ const de = { | |||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       action: { | ||||
|         erase: { | ||||
|           title: "Gemeldetes Event löschen", | ||||
|           content: | ||||
|             "Sind Sie sicher dass Sie das gemeldete Event löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     connections: { | ||||
|       name: "Verbindungen", | ||||
|  |  | |||
|  | @ -207,13 +207,6 @@ const en = { | |||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       action: { | ||||
|         erase: { | ||||
|           title: "Delete reported event", | ||||
|           content: | ||||
|             "Are you sure you want to delete the reported event? This cannot be undone.", | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     connections: { | ||||
|       name: "Connections", | ||||
|  |  | |||
							
								
								
									
										5
									
								
								src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/index.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import React from "react"; | ||||
| import ReactDOM from "react-dom"; | ||||
| import App from "./App"; | ||||
| 
 | ||||
| ReactDOM.render(<App />, document.getElementById("root")); | ||||
|  | @ -1,9 +0,0 @@ | |||
| import React from "react"; | ||||
| import { createRoot } from "react-dom/client"; | ||||
| import App from "./App"; | ||||
| 
 | ||||
| createRoot(document.getElementById("root")).render( | ||||
|   <React.StrictMode> | ||||
|     <App /> | ||||
|   </React.StrictMode> | ||||
| ); | ||||
|  | @ -98,7 +98,7 @@ const resourceMap = { | |||
|     }), | ||||
|     delete: params => ({ | ||||
|       endpoint: `/_synapse/admin/v2/users/${encodeURIComponent( | ||||
|         params.previousData.user_id | ||||
|         params.user_id | ||||
|       )}/devices/${params.id}`,
 | ||||
|     }), | ||||
|   }, | ||||
|  | @ -184,9 +184,9 @@ const resourceMap = { | |||
|     delete: params => ({ | ||||
|       endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( | ||||
|         "home_server" | ||||
|       )}/delete?before_ts=${params.meta.before_ts}&size_gt=${ | ||||
|         params.meta.size_gt | ||||
|       }&keep_profiles=${params.meta.keep_profiles}`,
 | ||||
|       )}/delete?before_ts=${params.before_ts}&size_gt=${ | ||||
|         params.size_gt | ||||
|       }&keep_profiles=${params.keep_profiles}`,
 | ||||
|       method: "POST", | ||||
|     }), | ||||
|   }, | ||||
|  | @ -197,7 +197,7 @@ const resourceMap = { | |||
|       method: "POST", | ||||
|     }), | ||||
|     delete: params => ({ | ||||
|       endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`, | ||||
|       endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`, | ||||
|       method: "POST", | ||||
|     }), | ||||
|   }, | ||||
|  | @ -212,7 +212,7 @@ const resourceMap = { | |||
|     delete: params => ({ | ||||
|       endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem( | ||||
|         "home_server" | ||||
|       )}/${params.id}`,
 | ||||
|       )}/${params.media_id}`,
 | ||||
|       method: "POST", | ||||
|     }), | ||||
|   }, | ||||
|  | @ -456,7 +456,7 @@ const dataProvider = { | |||
|     const res = resourceMap[resource]; | ||||
| 
 | ||||
|     const endpoint_url = homeserver + res.path; | ||||
|     return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, { | ||||
|     return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, { | ||||
|       method: "PUT", | ||||
|       body: JSON.stringify(params.data, filterNullValues), | ||||
|     }).then(({ json }) => ({ | ||||
|  | @ -546,7 +546,7 @@ const dataProvider = { | |||
|       const endpoint_url = homeserver + res.path; | ||||
|       return jsonClient(`${endpoint_url}/${params.id}`, { | ||||
|         method: "DELETE", | ||||
|         body: JSON.stringify(params.previousData, filterNullValues), | ||||
|         body: JSON.stringify(params.data, filterNullValues), | ||||
|       }).then(({ json }) => ({ | ||||
|         data: json, | ||||
|       })); | ||||
|  |  | |||
|  | @ -1,48 +0,0 @@ | |||
| import { fetchUtils } from "react-admin"; | ||||
| 
 | ||||
| export const splitMxid = mxid => { | ||||
|   const re = | ||||
|     /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/; | ||||
|   return re.exec(mxid)?.groups; | ||||
| }; | ||||
| 
 | ||||
| export const isValidBaseUrl = baseUrl => | ||||
|   /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl); | ||||
| 
 | ||||
| /** | ||||
|  * Resolve the homeserver URL using the well-known lookup | ||||
|  * @param domain  the domain part of an MXID | ||||
|  * @returns homeserver base URL | ||||
|  */ | ||||
| export const getWellKnownUrl = async domain => { | ||||
|   const wellKnownUrl = `https://${domain}/.well-known/matrix/client`; | ||||
|   try { | ||||
|     const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" }); | ||||
|     return json["m.homeserver"].base_url; | ||||
|   } catch { | ||||
|     // if there is no .well-known entry, return the domain itself
 | ||||
|     return `https://${domain}`; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get synapse server version | ||||
|  * @param base_url  the base URL of the homeserver | ||||
|  * @returns server version | ||||
|  */ | ||||
| export const getServerVersion = async baseUrl => { | ||||
|   const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`; | ||||
|   const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" }); | ||||
|   return response.json.server_version; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Get supported login flows | ||||
|  * @param baseUrl  the base URL of the homeserver | ||||
|  * @returns array of supported login flows | ||||
|  */ | ||||
| export const getSupportedLoginFlows = async baseUrl => { | ||||
|   const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`; | ||||
|   const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" }); | ||||
|   return response.json.flows; | ||||
| }; | ||||
|  | @ -1,31 +0,0 @@ | |||
| import { isValidBaseUrl, splitMxid } from "./synapse"; | ||||
| 
 | ||||
| describe("splitMxid", () => { | ||||
|   it("splits valid MXIDs", () => | ||||
|     expect(splitMxid("@name:domain.tld")).toEqual({ | ||||
|       name: "name", | ||||
|       domain: "domain.tld", | ||||
|     })); | ||||
|   it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined()); | ||||
| }); | ||||
| 
 | ||||
| describe("isValidBaseUrl", () => { | ||||
|   it("accepts a http URL", () => | ||||
|     expect(isValidBaseUrl("http://foo.bar")).toBeTruthy()); | ||||
|   it("accepts a https URL", () => | ||||
|     expect(isValidBaseUrl("https://foo.bar")).toBeTruthy()); | ||||
|   it("accepts a valid URL with port", () => | ||||
|     expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy()); | ||||
|   it("rejects undefined base URLs", () => | ||||
|     expect(isValidBaseUrl(undefined)).toBeFalsy()); | ||||
|   it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy()); | ||||
|   it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy()); | ||||
|   it("rejects non-string base URLs", () => | ||||
|     expect(isValidBaseUrl({})).toBeFalsy()); | ||||
|   it("rejects base URLs without protocol", () => | ||||
|     expect(isValidBaseUrl("foo.bar")).toBeFalsy()); | ||||
|   it("rejects base URLs with path", () => | ||||
|     expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy()); | ||||
|   it("rejects invalid base URLs", () => | ||||
|     expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy()); | ||||
| }); | ||||
		Loading…
	
		Reference in a new issue