mirror of
				https://github.com/UA-Fediland/synapse-admin.git
				synced 2025-10-31 06:08:28 +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 |       - name: Checkout | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       - name: Setup node |       - name: Setup node | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: "18" |           node-version: "18" | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: yarn --immutable |         run: yarn --frozen-lockfile | ||||||
|       - name: Run tests |       - name: Run tests | ||||||
|         run: yarn test |         run: yarn test | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								.github/workflows/docker-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/docker-release.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -19,11 +19,11 @@ jobs: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v3 |         uses: docker/setup-qemu-action@v2 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3 |         uses: docker/setup-buildx-action@v2 | ||||||
|       - name: Login to DockerHub |       - name: Login to DockerHub | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|  | @ -43,7 +43,7 @@ jobs: | ||||||
|           esac |           esac | ||||||
|           echo "::set-output name=tag::$tag" |           echo "::set-output name=tag::$tag" | ||||||
|       - name: Build and Push Tag |       - name: Build and Push Tag | ||||||
|         uses: docker/build-push-action@v5 |         uses: docker/build-push-action@v4 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           push: true |           push: true | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								.github/workflows/edge_ghpage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/edge_ghpage.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -11,16 +11,16 @@ jobs: | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout 🛎️ |       - name: Checkout 🛎️ | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       - uses: actions/setup-node@v4 |       - uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: "18" |           node-version: "18" | ||||||
|       - name: Install and Build 🔧 |       - name: Install and Build 🔧 | ||||||
|         run: | |         run: | | ||||||
|           yarn install --immutable |           yarn install | ||||||
|           yarn build |           yarn build | ||||||
| 
 | 
 | ||||||
|       - name: Deploy 🚀 |       - name: Deploy 🚀 | ||||||
|         uses: JamesIves/github-pages-deploy-action@v4.5.0 |         uses: JamesIves/github-pages-deploy-action@v4.4.3 | ||||||
|         with: |         with: | ||||||
|           branch: gh-pages |           branch: gh-pages | ||||||
|           folder: build |           folder: build | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								.github/workflows/github-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/github-release.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -14,10 +14,10 @@ jobs: | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - uses: actions/setup-node@v4 |       - uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: "18" |           node-version: "18" | ||||||
|       - run: yarn install --immutable |       - run: yarn install | ||||||
|       - run: yarn build |       - run: yarn build | ||||||
|       - run: | |       - run: | | ||||||
|           version=`git describe --dirty --tags || echo unknown` |           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 |       - name: Checkout | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v3 |         uses: docker/setup-qemu-action@v2 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3 |         uses: docker/setup-buildx-action@v2 | ||||||
|       - name: Login to DockerHub |       - name: Login to DockerHub | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|  | @ -43,7 +43,7 @@ jobs: | ||||||
|           esac |           esac | ||||||
|           echo "::set-output name=tag::$tag" |           echo "::set-output name=tag::$tag" | ||||||
|       - name: Build and Push Tag |       - name: Build and Push Tag | ||||||
|         uses: docker/build-push-action@v5 |         uses: docker/build-push-action@v4 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           push: false |           push: false | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ ARG REACT_APP_SERVER | ||||||
| WORKDIR /src | WORKDIR /src | ||||||
| 
 | 
 | ||||||
| COPY . /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 | 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 | ### 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`. | 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. | 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` | - `/_matrix` | ||||||
| - `/_synapse/admin` | - `/_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 | ### Use without install | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								package.json
									
									
									
									
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "synapse-admin", |   "name": "synapse-admin", | ||||||
|   "version": "0.9.1", |   "version": "0.8.5", | ||||||
|   "description": "Admin GUI for the Matrix.org server Synapse", |   "description": "Admin GUI for the Matrix.org server Synapse", | ||||||
|   "author": "Awesome Technologies Innovationslabor GmbH", |   "author": "Awesome Technologies Innovationslabor GmbH", | ||||||
|   "license": "Apache-2.0", |   "license": "Apache-2.0", | ||||||
|  | @ -10,28 +10,31 @@ | ||||||
|     "url": "https://github.com/Awesome-Technologies/synapse-admin" |     "url": "https://github.com/Awesome-Technologies/synapse-admin" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@testing-library/jest-dom": "^6.0.0", |     "@testing-library/jest-dom": "^5.16.5", | ||||||
|     "@testing-library/react": "^14.0.0", |     "@testing-library/react": "^12.1.5", | ||||||
|     "@testing-library/user-event": "^14.5.2", |     "@testing-library/user-event": "^14.4.3", | ||||||
|     "eslint": "^8.56.0", |     "eslint": "^8.48.0", | ||||||
|     "eslint-config-prettier": "^9.1.0", |     "eslint-config-prettier": "^9.0.0", | ||||||
|     "eslint-config-react-app": "^7.0.1", |     "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", |     "jest-fetch-mock": "^3.0.3", | ||||||
|     "prettier": "^3.2.5" |     "prettier": "^2.2.0", | ||||||
|  |     "ra-test": "^3.19.12" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@mui/icons-material": "^5.15.10", |     "@emotion/react": "^11.11.1", | ||||||
|     "@mui/material": "^5.15.10", |     "@emotion/styled": "^11.10.6", | ||||||
|     "@mui/styles": "^5.15.10", |     "@mui/icons-material": "^5.14.8", | ||||||
|  |     "@mui/material": "^5.14.8", | ||||||
|     "papaparse": "^5.4.1", |     "papaparse": "^5.4.1", | ||||||
|  |     "prop-types": "^15.8.1", | ||||||
|     "ra-language-chinese": "^2.0.10", |     "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-german": "^3.13.4", | ||||||
|     "ra-language-italian": "^3.13.1", |     "ra-language-italian": "^3.13.1", | ||||||
|     "react": "^18.0.0", |     "react": "^17.0.0", | ||||||
|     "react-admin": "^4.16.11", |     "react-admin": "^3.19.12", | ||||||
|     "react-dom": "^18.0.0", |     "react-dom": "^17.0.2", | ||||||
|     "react-scripts": "^5.0.1" |     "react-scripts": "^5.0.1" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  |  | ||||||
|  | @ -1,3 +1,3 @@ | ||||||
| id,displayname,password,is_guest,admin,deactivated | 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 | ,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 { | import { | ||||||
|   Datagrid, |   Datagrid, | ||||||
|   DateField, |   DateField, | ||||||
|   DeleteButton, |  | ||||||
|   List, |   List, | ||||||
|   NumberField, |   NumberField, | ||||||
|   Pagination, |   Pagination, | ||||||
|  | @ -11,12 +10,9 @@ import { | ||||||
|   Tab, |   Tab, | ||||||
|   TabbedShowLayout, |   TabbedShowLayout, | ||||||
|   TextField, |   TextField, | ||||||
|   TopToolbar, |  | ||||||
|   useRecordContext, |  | ||||||
|   useTranslate, |   useTranslate, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import PageviewIcon from "@mui/icons-material/Pageview"; | import PageviewIcon from "@mui/icons-material/Pageview"; | ||||||
| import ReportIcon from "@mui/icons-material/Warning"; |  | ||||||
| import ViewListIcon from "@mui/icons-material/ViewList"; | import ViewListIcon from "@mui/icons-material/ViewList"; | ||||||
| 
 | 
 | ||||||
| const date_format = { | const date_format = { | ||||||
|  | @ -28,14 +24,14 @@ const date_format = { | ||||||
|   second: "2-digit", |   second: "2-digit", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const ReportPagination = () => ( | const ReportPagination = props => ( | ||||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> |   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const ReportShow = props => { | export const ReportShow = props => { | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   return ( |   return ( | ||||||
|     <Show {...props} actions={<ReportShowActions />}> |     <Show {...props}> | ||||||
|       <TabbedShowLayout> |       <TabbedShowLayout> | ||||||
|         <Tab |         <Tab | ||||||
|           label={translate("synapseadmin.reports.tabs.basic", { |           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.algorithm" /> | ||||||
|           <TextField |           <TextField | ||||||
|             source="event_json.content.device_id" |             source="event_json.content.device_id" | ||||||
|             label="resources.devices.fields.device_id" |             label="resources.users.fields.device_id" | ||||||
|           /> |           /> | ||||||
|         </Tab> |         </Tab> | ||||||
|       </TabbedShowLayout> |       </TabbedShowLayout> | ||||||
|  | @ -102,28 +98,15 @@ export const ReportShow = props => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const ReportShowActions = () => { | export const ReportList = ({ ...props }) => { | ||||||
|   const record = useRecordContext(); |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <TopToolbar> |  | ||||||
|       <DeleteButton |  | ||||||
|         record={record} |  | ||||||
|         mutationMode="pessimistic" |  | ||||||
|         confirmTitle="resources.reports.action.erase.title" |  | ||||||
|         confirmContent="resources.reports.action.erase.content" |  | ||||||
|       /> |  | ||||||
|     </TopToolbar> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const ReportList = props => ( |  | ||||||
|     <List |     <List | ||||||
|       {...props} |       {...props} | ||||||
|       pagination={<ReportPagination />} |       pagination={<ReportPagination />} | ||||||
|       sort={{ field: "received_ts", order: "DESC" }} |       sort={{ field: "received_ts", order: "DESC" }} | ||||||
|  |       bulkActionButtons={false} | ||||||
|     > |     > | ||||||
|     <Datagrid rowClick="show" bulkActionButtons={false}> |       <Datagrid rowClick="show"> | ||||||
|         <TextField source="id" sortable={false} /> |         <TextField source="id" sortable={false} /> | ||||||
|         <DateField |         <DateField | ||||||
|           source="received_ts" |           source="received_ts" | ||||||
|  | @ -137,12 +120,4 @@ export const ReportList = props => ( | ||||||
|       </Datagrid> |       </Datagrid> | ||||||
|     </List> |     </List> | ||||||
|   ); |   ); | ||||||
| 
 |  | ||||||
| const resource = { |  | ||||||
|   name: "reports", |  | ||||||
|   icon: ReportIcon, |  | ||||||
|   list: ReportList, |  | ||||||
|   show: ReportShow, |  | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| export default resource; |  | ||||||
|  | @ -1,6 +1,12 @@ | ||||||
| import React, { useState } from "react"; | 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 { parse as parseCsv, unparse as unparseCsv } from "papaparse"; | ||||||
|  | import GetAppIcon from "@mui/icons-material/GetApp"; | ||||||
| import { | import { | ||||||
|   Button, |   Button, | ||||||
|   Card, |   Card, | ||||||
|  | @ -17,6 +23,19 @@ import { generateRandomUser } from "./users"; | ||||||
| 
 | 
 | ||||||
| const LOGGING = true; | 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 expectedFields = ["id", "displayname"].sort(); | ||||||
| const optionalFields = [ | const optionalFields = [ | ||||||
|   "user_type", |   "user_type", | ||||||
|  | @ -32,7 +51,7 @@ function TranslatableOption({ value, text }) { | ||||||
|   return <option value={value}>{translate(text)}</option>; |   return <option value={value}>{translate(text)}</option>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const FilePicker = () => { | const FilePicker = props => { | ||||||
|   const [values, setValues] = useState(null); |   const [values, setValues] = useState(null); | ||||||
|   const [error, setError] = useState(null); |   const [error, setError] = useState(null); | ||||||
|   const [stats, setStats] = useState(null); |   const [stats, setStats] = useState(null); | ||||||
|  | @ -191,7 +210,7 @@ const FilePicker = () => { | ||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const runImport = async _e => { |   const runImport = async e => { | ||||||
|     if (progress !== null) { |     if (progress !== null) { | ||||||
|       notify("import_users.errors.already_in_progress"); |       notify("import_users.errors.already_in_progress"); | ||||||
|       return; |       return; | ||||||
|  | @ -307,7 +326,7 @@ const FilePicker = () => { | ||||||
|         let retries = 0; |         let retries = 0; | ||||||
|         const submitRecord = recordData => { |         const submitRecord = recordData => { | ||||||
|           return dataProvider.getOne("users", { id: recordData.id }).then( |           return dataProvider.getOne("users", { id: recordData.id }).then( | ||||||
|             async _alreadyExists => { |             async alreadyExists => { | ||||||
|               if (LOGGING) console.log("already existed"); |               if (LOGGING) console.log("already existed"); | ||||||
| 
 | 
 | ||||||
|               if (useridMode === "update" || conflictMode === "skip") { |               if (useridMode === "update" || conflictMode === "skip") { | ||||||
|  | @ -332,7 +351,7 @@ const FilePicker = () => { | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|             }, |             }, | ||||||
|             async _okToSubmit => { |             async okToSubmit => { | ||||||
|               if (LOGGING) |               if (LOGGING) | ||||||
|                 console.log( |                 console.log( | ||||||
|                   "OK to create record " + |                   "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 React from "react"; | ||||||
| import { render } from "@testing-library/react"; | import { render } from "@testing-library/react"; | ||||||
| import { AdminContext } from "react-admin"; | import { TestContext } from "ra-test"; | ||||||
| import LoginPage from "./LoginPage"; | import LoginPage from "./LoginPage"; | ||||||
| 
 | 
 | ||||||
| describe("LoginForm", () => { | describe("LoginForm", () => { | ||||||
|   it("renders", () => { |   it("renders", () => { | ||||||
|     render( |     render( | ||||||
|       <AdminContext> |       <TestContext> | ||||||
|         <LoginPage /> |         <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, |   DateField, | ||||||
|   DateTimeInput, |   DateTimeInput, | ||||||
|   Edit, |   Edit, | ||||||
|  |   Filter, | ||||||
|   List, |   List, | ||||||
|   maxValue, |   maxValue, | ||||||
|   number, |   number, | ||||||
|   NumberField, |   NumberField, | ||||||
|   NumberInput, |   NumberInput, | ||||||
|   regex, |   regex, | ||||||
|   SaveButton, |  | ||||||
|   SimpleForm, |   SimpleForm, | ||||||
|   TextInput, |   TextInput, | ||||||
|   TextField, |   TextField, | ||||||
|   Toolbar, |   Toolbar, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber"; |  | ||||||
| 
 | 
 | ||||||
| const date_format = { | const date_format = { | ||||||
|   year: "numeric", |   year: "numeric", | ||||||
|  | @ -54,12 +53,17 @@ const dateFormatter = v => { | ||||||
|   return `${year}-${month}-${day}T${hour}:${minute}`; |   return `${year}-${month}-${day}T${hour}:${minute}`; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />]; | const RegistrationTokenFilter = props => ( | ||||||
|  |   <Filter {...props}> | ||||||
|  |     <BooleanInput source="valid" alwaysOn /> | ||||||
|  |   </Filter> | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| export const RegistrationTokenList = props => ( | export const RegistrationTokenList = props => { | ||||||
|  |   return ( | ||||||
|     <List |     <List | ||||||
|       {...props} |       {...props} | ||||||
|     filters={registrationTokenFilters} |       filters={<RegistrationTokenFilter />} | ||||||
|       filterDefaultValues={{ valid: true }} |       filterDefaultValues={{ valid: true }} | ||||||
|       pagination={false} |       pagination={false} | ||||||
|       perPage={500} |       perPage={500} | ||||||
|  | @ -78,17 +82,11 @@ export const RegistrationTokenList = props => ( | ||||||
|       </Datagrid> |       </Datagrid> | ||||||
|     </List> |     </List> | ||||||
|   ); |   ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export const RegistrationTokenCreate = props => ( | export const RegistrationTokenCreate = props => ( | ||||||
|   <Create {...props} redirect="list"> |   <Create {...props}> | ||||||
|     <SimpleForm |     <SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}> | ||||||
|       toolbar={ |  | ||||||
|         <Toolbar> |  | ||||||
|           {/* It is possible to create tokens per default without input. */} |  | ||||||
|           <SaveButton alwaysEnable /> |  | ||||||
|         </Toolbar> |  | ||||||
|       } |  | ||||||
|     > |  | ||||||
|       <TextInput |       <TextInput | ||||||
|         source="token" |         source="token" | ||||||
|         autoComplete="off" |         autoComplete="off" | ||||||
|  | @ -111,7 +109,8 @@ export const RegistrationTokenCreate = props => ( | ||||||
|   </Create> |   </Create> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const RegistrationTokenEdit = props => ( | export const RegistrationTokenEdit = props => { | ||||||
|  |   return ( | ||||||
|     <Edit {...props}> |     <Edit {...props}> | ||||||
|       <SimpleForm> |       <SimpleForm> | ||||||
|         <TextInput source="token" disabled /> |         <TextInput source="token" disabled /> | ||||||
|  | @ -130,13 +129,4 @@ export const RegistrationTokenEdit = props => ( | ||||||
|       </SimpleForm> |       </SimpleForm> | ||||||
|     </Edit> |     </Edit> | ||||||
|   ); |   ); | ||||||
| 
 |  | ||||||
| const resource = { |  | ||||||
|   name: "registration_tokens", |  | ||||||
|   icon: RegistrationTokenIcon, |  | ||||||
|   list: RegistrationTokenList, |  | ||||||
|   edit: RegistrationTokenEdit, |  | ||||||
|   create: RegistrationTokenCreate, |  | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| 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 { | import { | ||||||
|   Button, |   Button, | ||||||
|   SaveButton, |   SaveButton, | ||||||
|  | @ -7,14 +7,12 @@ import { | ||||||
|   Toolbar, |   Toolbar, | ||||||
|   required, |   required, | ||||||
|   useCreate, |   useCreate, | ||||||
|   useDataProvider, |   useMutation, | ||||||
|   useListContext, |  | ||||||
|   useNotify, |   useNotify, | ||||||
|   useRecordContext, |   useRecordContext, | ||||||
|   useTranslate, |   useTranslate, | ||||||
|   useUnselectAll, |   useUnselectAll, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import { useMutation } from "react-query"; |  | ||||||
| import MessageIcon from "@mui/icons-material/Message"; | import MessageIcon from "@mui/icons-material/Message"; | ||||||
| import IconCancel from "@mui/icons-material/Cancel"; | import IconCancel from "@mui/icons-material/Cancel"; | ||||||
| import { | import { | ||||||
|  | @ -24,7 +22,7 @@ import { | ||||||
|   DialogTitle, |   DialogTitle, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| 
 | 
 | ||||||
| const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => { | const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => { | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
| 
 | 
 | ||||||
|   const ServerNoticeToolbar = props => ( |   const ServerNoticeToolbar = props => ( | ||||||
|  | @ -48,7 +46,12 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => { | ||||||
|         <DialogContentText> |         <DialogContentText> | ||||||
|           {translate("resources.servernotices.helper.send")} |           {translate("resources.servernotices.helper.send")} | ||||||
|         </DialogContentText> |         </DialogContentText> | ||||||
|         <SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}> |         <SimpleForm | ||||||
|  |           toolbar={<ServerNoticeToolbar />} | ||||||
|  |           submitOnEnter={false} | ||||||
|  |           redirect={false} | ||||||
|  |           save={onSend} | ||||||
|  |         > | ||||||
|           <TextInput |           <TextInput | ||||||
|             source="body" |             source="body" | ||||||
|             label="resources.servernotices.fields.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 record = useRecordContext(); | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const [create, { isloading }] = useCreate(); |   const [create, { loading }] = useCreate("servernotices"); | ||||||
| 
 | 
 | ||||||
|   const handleDialogOpen = () => setOpen(true); |   const handleDialogOpen = () => setOpen(true); | ||||||
|   const handleDialogClose = () => setOpen(false); |   const handleDialogClose = () => setOpen(false); | ||||||
| 
 | 
 | ||||||
|   const handleSend = values => { |   const handleSend = values => { | ||||||
|     create( |     create( | ||||||
|       "servernotices", |       { payload: { data: { id: record.id, ...values } } }, | ||||||
|       { data: { id: record.id, ...values } }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.servernotices.action.send_success"); |           notify("resources.servernotices.action.send_success"); | ||||||
|           handleDialogClose(); |           handleDialogClose(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.servernotices.action.send_failure", { |           notify("resources.servernotices.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -91,65 +93,67 @@ export const ServerNoticeButton = () => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <Fragment> | ||||||
|       <Button |       <Button | ||||||
|         label="resources.servernotices.send" |         label="resources.servernotices.send" | ||||||
|         onClick={handleDialogOpen} |         onClick={handleDialogOpen} | ||||||
|         disabled={isloading} |         disabled={loading} | ||||||
|       > |       > | ||||||
|         <MessageIcon /> |         <MessageIcon /> | ||||||
|       </Button> |       </Button> | ||||||
|       <ServerNoticeDialog |       <ServerNoticeDialog | ||||||
|         open={open} |         open={open} | ||||||
|         onClose={handleDialogClose} |         onClose={handleDialogClose} | ||||||
|         onSubmit={handleSend} |         onSend={handleSend} | ||||||
|       /> |       /> | ||||||
|     </> |     </Fragment> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const ServerNoticeBulkButton = () => { | export const ServerNoticeBulkButton = ({ selectedIds }) => { | ||||||
|   const { selectedIds } = useListContext(); |  | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const openDialog = () => setOpen(true); |  | ||||||
|   const closeDialog = () => setOpen(false); |  | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const unselectAllUsers = useUnselectAll("users"); |   const unselectAll = useUnselectAll(); | ||||||
|   const dataProvider = useDataProvider(); |   const [createMany, { loading }] = useMutation(); | ||||||
| 
 | 
 | ||||||
|   const { mutate: sendNotices, isLoading } = useMutation( |   const handleDialogOpen = () => setOpen(true); | ||||||
|     data => |   const handleDialogClose = () => setOpen(false); | ||||||
|       dataProvider.createMany("servernotices", { | 
 | ||||||
|         ids: selectedIds, |   const handleSend = values => { | ||||||
|         data: data, |     createMany( | ||||||
|       }), |  | ||||||
|       { |       { | ||||||
|       onSuccess: () => { |         type: "createMany", | ||||||
|         notify("resources.servernotices.action.send_success"); |         resource: "servernotices", | ||||||
|         unselectAllUsers(); |         payload: { ids: selectedIds, data: values }, | ||||||
|         closeDialog(); |  | ||||||
|       }, |       }, | ||||||
|       onError: () => |       { | ||||||
|  |         onSuccess: ({ data }) => { | ||||||
|  |           notify("resources.servernotices.action.send_success"); | ||||||
|  |           unselectAll("users"); | ||||||
|  |           handleDialogClose(); | ||||||
|  |         }, | ||||||
|  |         onFailure: error => | ||||||
|           notify("resources.servernotices.action.send_failure", { |           notify("resources.servernotices.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <Fragment> | ||||||
|       <Button |       <Button | ||||||
|         label="resources.servernotices.send" |         label="resources.servernotices.send" | ||||||
|         onClick={openDialog} |         onClick={handleDialogOpen} | ||||||
|         disabled={isLoading} |         disabled={loading} | ||||||
|       > |       > | ||||||
|         <MessageIcon /> |         <MessageIcon /> | ||||||
|       </Button> |       </Button> | ||||||
|       <ServerNoticeDialog |       <ServerNoticeDialog | ||||||
|         open={open} |         open={open} | ||||||
|         onClose={closeDialog} |         onClose={handleDialogClose} | ||||||
|         onSubmit={sendNotices} |         onSend={handleSend} | ||||||
|       /> |       /> | ||||||
|     </> |     </Fragment> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | @ -3,6 +3,7 @@ import { | ||||||
|   Button, |   Button, | ||||||
|   Datagrid, |   Datagrid, | ||||||
|   DateField, |   DateField, | ||||||
|  |   Filter, | ||||||
|   List, |   List, | ||||||
|   Pagination, |   Pagination, | ||||||
|   ReferenceField, |   ReferenceField, | ||||||
|  | @ -20,12 +21,11 @@ import { | ||||||
|   useTranslate, |   useTranslate, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import AutorenewIcon from "@mui/icons-material/Autorenew"; | import AutorenewIcon from "@mui/icons-material/Autorenew"; | ||||||
| import DestinationsIcon from "@mui/icons-material/CloudQueue"; |  | ||||||
| import FolderSharedIcon from "@mui/icons-material/FolderShared"; | import FolderSharedIcon from "@mui/icons-material/FolderShared"; | ||||||
| import ViewListIcon from "@mui/icons-material/ViewList"; | import ViewListIcon from "@mui/icons-material/ViewList"; | ||||||
| 
 | 
 | ||||||
| const DestinationPagination = () => ( | const DestinationPagination = props => ( | ||||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> |   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const date_format = { | const date_format = { | ||||||
|  | @ -37,17 +37,23 @@ const date_format = { | ||||||
|   second: "2-digit", |   second: "2-digit", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const destinationRowSx = (record, _index) => ({ | const destinationRowStyle = (record, index) => ({ | ||||||
|   backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white", |   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 record = useRecordContext(); | ||||||
|   const refresh = useRefresh(); |   const refresh = useRefresh(); | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const [handleReconnect, { isLoading }] = useDelete(); |   const [handleReconnect, { isLoading }] = useDelete("destinations"); | ||||||
| 
 | 
 | ||||||
|   // Reconnect is not required if no error has occurred. (`failure_ts`)
 |   // Reconnect is not required if no error has occurred. (`failure_ts`)
 | ||||||
|   if (!record || !record.failure_ts) return null; |   if (!record || !record.failure_ts) return null; | ||||||
|  | @ -57,8 +63,7 @@ export const DestinationReconnectButton = () => { | ||||||
|     e.stopPropagation(); |     e.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|     handleReconnect( |     handleReconnect( | ||||||
|       "destinations", |       { payload: { id: record.id } }, | ||||||
|       { id: record.id }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("ra.notification.updated", { |           notify("ra.notification.updated", { | ||||||
|  | @ -66,7 +71,7 @@ export const DestinationReconnectButton = () => { | ||||||
|           }); |           }); | ||||||
|           refresh(); |           refresh(); | ||||||
|         }, |         }, | ||||||
|         onError: () => { |         onFailure: () => { | ||||||
|           notify("ra.message.error", { type: "error" }); |           notify("ra.message.error", { type: "error" }); | ||||||
|         }, |         }, | ||||||
|       } |       } | ||||||
|  | @ -84,13 +89,13 @@ export const DestinationReconnectButton = () => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const DestinationShowActions = () => ( | const DestinationShowActions = props => ( | ||||||
|   <TopToolbar> |   <TopToolbar> | ||||||
|     <DestinationReconnectButton /> |     <DestinationReconnectButton /> | ||||||
|   </TopToolbar> |   </TopToolbar> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const DestinationTitle = () => { | const DestinationTitle = props => { | ||||||
|   const record = useRecordContext(); |   const record = useRecordContext(); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   return ( |   return ( | ||||||
|  | @ -104,14 +109,14 @@ export const DestinationList = props => { | ||||||
|   return ( |   return ( | ||||||
|     <List |     <List | ||||||
|       {...props} |       {...props} | ||||||
|       filters={destinationFilters} |       filters={<DestinationFilter />} | ||||||
|       pagination={<DestinationPagination />} |       pagination={<DestinationPagination />} | ||||||
|       sort={{ field: "destination", order: "ASC" }} |       sort={{ field: "destination", order: "ASC" }} | ||||||
|  |       bulkActionButtons={false} | ||||||
|     > |     > | ||||||
|       <Datagrid |       <Datagrid | ||||||
|         rowSx={destinationRowSx} |         rowStyle={destinationRowStyle} | ||||||
|         rowClick={(id, _resource, _record) => `${id}/show/rooms`} |         rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`} | ||||||
|         bulkActionButtons={false} |  | ||||||
|       > |       > | ||||||
|         <TextField source="destination" /> |         <TextField source="destination" /> | ||||||
|         <DateField source="failure_ts" showTime options={date_format} /> |         <DateField source="failure_ts" showTime options={date_format} /> | ||||||
|  | @ -155,7 +160,7 @@ export const DestinationShow = props => { | ||||||
|           > |           > | ||||||
|             <Datagrid |             <Datagrid | ||||||
|               style={{ width: "100%" }} |               style={{ width: "100%" }} | ||||||
|               rowClick={(id, resource, record) => `/rooms/${id}/show`} |               rowClick={(id, basePath, record) => `/rooms/${id}/show`} | ||||||
|             > |             > | ||||||
|               <TextField |               <TextField | ||||||
|                 source="room_id" |                 source="room_id" | ||||||
|  | @ -178,12 +183,3 @@ export const DestinationShow = props => { | ||||||
|     </Show> |     </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 { | import { | ||||||
|   BooleanInput, |   BooleanInput, | ||||||
|   Button, |   Button, | ||||||
|  | @ -27,9 +30,24 @@ import { | ||||||
| import IconCancel from "@mui/icons-material/Cancel"; | import IconCancel from "@mui/icons-material/Cancel"; | ||||||
| import LockIcon from "@mui/icons-material/Lock"; | import LockIcon from "@mui/icons-material/Lock"; | ||||||
| import LockOpenIcon from "@mui/icons-material/LockOpen"; | 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 translate = useTranslate(); | ||||||
| 
 | 
 | ||||||
|   const dateParser = v => { |   const dateParser = v => { | ||||||
|  | @ -38,7 +56,8 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | ||||||
|     return d.getTime(); |     return d.getTime(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const DeleteMediaToolbar = props => ( |   const DeleteMediaToolbar = props => { | ||||||
|  |     return ( | ||||||
|       <Toolbar {...props}> |       <Toolbar {...props}> | ||||||
|         <SaveButton |         <SaveButton | ||||||
|           label="resources.delete_media.action.send" |           label="resources.delete_media.action.send" | ||||||
|  | @ -49,6 +68,7 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | ||||||
|         </Button> |         </Button> | ||||||
|       </Toolbar> |       </Toolbar> | ||||||
|     ); |     ); | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Dialog open={open} onClose={onClose} loading={loading}> |     <Dialog open={open} onClose={onClose} loading={loading}> | ||||||
|  | @ -59,7 +79,12 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | ||||||
|         <DialogContentText> |         <DialogContentText> | ||||||
|           {translate("resources.delete_media.helper.send")} |           {translate("resources.delete_media.helper.send")} | ||||||
|         </DialogContentText> |         </DialogContentText> | ||||||
|         <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}> |         <SimpleForm | ||||||
|  |           toolbar={<DeleteMediaToolbar />} | ||||||
|  |           submitOnEnter={false} | ||||||
|  |           redirect={false} | ||||||
|  |           save={onSend} | ||||||
|  |         > | ||||||
|           <DateTimeInput |           <DateTimeInput | ||||||
|             fullWidth |             fullWidth | ||||||
|             source="before_ts" |             source="before_ts" | ||||||
|  | @ -88,25 +113,23 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const DeleteMediaButton = props => { | export const DeleteMediaButton = props => { | ||||||
|   const theme = useTheme(); |   const classes = useStyles(props); | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const [deleteOne, { isLoading }] = useDelete(); |   const [deleteOne, { loading }] = useDelete("delete_media"); | ||||||
| 
 | 
 | ||||||
|   const openDialog = () => setOpen(true); |   const handleDialogOpen = () => setOpen(true); | ||||||
|   const closeDialog = () => setOpen(false); |   const handleDialogClose = () => setOpen(false); | ||||||
| 
 | 
 | ||||||
|   const deleteMedia = values => { |   const handleSend = values => { | ||||||
|     deleteOne( |     deleteOne( | ||||||
|       "delete_media", |       { payload: { ...values } }, | ||||||
|       // needs meta.before_ts, meta.size_gt and meta.keep_profiles
 |  | ||||||
|       { meta: values }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.delete_media.action.send_success"); |           notify("resources.delete_media.action.send_success"); | ||||||
|           closeDialog(); |           handleDialogClose(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.delete_media.action.send_failure", { |           notify("resources.delete_media.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -115,54 +138,43 @@ export const DeleteMediaButton = props => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <Fragment> | ||||||
|       <Button |       <Button | ||||||
|         {...props} |  | ||||||
|         label="resources.delete_media.action.send" |         label="resources.delete_media.action.send" | ||||||
|         onClick={openDialog} |         onClick={handleDialogOpen} | ||||||
|         disabled={isLoading} |         disabled={loading} | ||||||
|         sx={{ |         className={classnames("ra-delete-button", classes.deleteButton)} | ||||||
|           color: theme.palette.error.main, |  | ||||||
|           "&:hover": { |  | ||||||
|             backgroundColor: alpha(theme.palette.error.main, 0.12), |  | ||||||
|             // Reset on mouse devices
 |  | ||||||
|             "@media (hover: none)": { |  | ||||||
|               backgroundColor: "transparent", |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }} |  | ||||||
|       > |       > | ||||||
|         <DeleteSweepIcon /> |         <DeleteSweepIcon /> | ||||||
|       </Button> |       </Button> | ||||||
|       <DeleteMediaDialog |       <DeleteMediaDialog | ||||||
|         open={open} |         open={open} | ||||||
|         onClose={closeDialog} |         onClose={handleDialogClose} | ||||||
|         onSubmit={deleteMedia} |         onSend={handleSend} | ||||||
|       /> |       /> | ||||||
|     </> |     </Fragment> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const ProtectMediaButton = () => { | export const ProtectMediaButton = props => { | ||||||
|   const record = useRecordContext(); |   const record = useRecordContext(); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   const refresh = useRefresh(); |   const refresh = useRefresh(); | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const [create, { isLoading }] = useCreate(); |   const [create, { loading }] = useCreate("protect_media"); | ||||||
|   const [deleteOne] = useDelete(); |   const [deleteOne] = useDelete("protect_media"); | ||||||
| 
 | 
 | ||||||
|   if (!record) return null; |   if (!record) return null; | ||||||
| 
 | 
 | ||||||
|   const handleProtect = () => { |   const handleProtect = () => { | ||||||
|     create( |     create( | ||||||
|       "protect_media", |       { payload: { data: record } }, | ||||||
|       { data: record }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.protect_media.action.send_success"); |           notify("resources.protect_media.action.send_success"); | ||||||
|           refresh(); |           refresh(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.protect_media.action.send_failure", { |           notify("resources.protect_media.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -172,14 +184,13 @@ export const ProtectMediaButton = () => { | ||||||
| 
 | 
 | ||||||
|   const handleUnprotect = () => { |   const handleUnprotect = () => { | ||||||
|     deleteOne( |     deleteOne( | ||||||
|       "protect_media", |       { payload: { ...record } }, | ||||||
|       { id: record.id }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.protect_media.action.send_success"); |           notify("resources.protect_media.action.send_success"); | ||||||
|           refresh(); |           refresh(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.protect_media.action.send_failure", { |           notify("resources.protect_media.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -192,7 +203,7 @@ export const ProtectMediaButton = () => { | ||||||
|     Wrapping Tooltip with <div> |     Wrapping Tooltip with <div> | ||||||
|     https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
 |     https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
 | ||||||
|     */ |     */ | ||||||
|     <> |     <Fragment> | ||||||
|       {record.quarantined_by && ( |       {record.quarantined_by && ( | ||||||
|         <Tooltip |         <Tooltip | ||||||
|           title={translate("resources.protect_media.action.none", { |           title={translate("resources.protect_media.action.none", { | ||||||
|  | @ -218,7 +229,7 @@ export const ProtectMediaButton = () => { | ||||||
|           arrow |           arrow | ||||||
|         > |         > | ||||||
|           <div> |           <div> | ||||||
|             <Button onClick={handleUnprotect} disabled={isLoading}> |             <Button onClick={handleUnprotect} disabled={loading}> | ||||||
|               <LockIcon /> |               <LockIcon /> | ||||||
|             </Button> |             </Button> | ||||||
|           </div> |           </div> | ||||||
|  | @ -231,13 +242,13 @@ export const ProtectMediaButton = () => { | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <div> |           <div> | ||||||
|             <Button onClick={handleProtect} disabled={isLoading}> |             <Button onClick={handleProtect} disabled={loading}> | ||||||
|               <LockOpenIcon /> |               <LockOpenIcon /> | ||||||
|             </Button> |             </Button> | ||||||
|           </div> |           </div> | ||||||
|         </Tooltip> |         </Tooltip> | ||||||
|       )} |       )} | ||||||
|     </> |     </Fragment> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -246,21 +257,20 @@ export const QuarantineMediaButton = props => { | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   const refresh = useRefresh(); |   const refresh = useRefresh(); | ||||||
|   const notify = useNotify(); |   const notify = useNotify(); | ||||||
|   const [create, { isLoading }] = useCreate(); |   const [create, { loading }] = useCreate("quarantine_media"); | ||||||
|   const [deleteOne] = useDelete(); |   const [deleteOne] = useDelete("quarantine_media"); | ||||||
| 
 | 
 | ||||||
|   if (!record) return null; |   if (!record) return null; | ||||||
| 
 | 
 | ||||||
|   const handleQuarantaine = () => { |   const handleQuarantaine = () => { | ||||||
|     create( |     create( | ||||||
|       "quarantine_media", |       { payload: { data: record } }, | ||||||
|       { data: record }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.quarantine_media.action.send_success"); |           notify("resources.quarantine_media.action.send_success"); | ||||||
|           refresh(); |           refresh(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.quarantine_media.action.send_failure", { |           notify("resources.quarantine_media.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -270,14 +280,13 @@ export const QuarantineMediaButton = props => { | ||||||
| 
 | 
 | ||||||
|   const handleRemoveQuarantaine = () => { |   const handleRemoveQuarantaine = () => { | ||||||
|     deleteOne( |     deleteOne( | ||||||
|       "quarantine_media", |       { payload: { ...record } }, | ||||||
|       { id: record.id, previousData: record }, |  | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           notify("resources.quarantine_media.action.send_success"); |           notify("resources.quarantine_media.action.send_success"); | ||||||
|           refresh(); |           refresh(); | ||||||
|         }, |         }, | ||||||
|         onError: () => |         onFailure: () => | ||||||
|           notify("resources.quarantine_media.action.send_failure", { |           notify("resources.quarantine_media.action.send_failure", { | ||||||
|             type: "error", |             type: "error", | ||||||
|           }), |           }), | ||||||
|  | @ -286,7 +295,7 @@ export const QuarantineMediaButton = props => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <Fragment> | ||||||
|       {record.safe_from_quarantine && ( |       {record.safe_from_quarantine && ( | ||||||
|         <Tooltip |         <Tooltip | ||||||
|           title={translate("resources.quarantine_media.action.none", { |           title={translate("resources.quarantine_media.action.none", { | ||||||
|  | @ -294,7 +303,7 @@ export const QuarantineMediaButton = props => { | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <div> |           <div> | ||||||
|             <Button {...props} disabled={true}> |             <Button disabled={true}> | ||||||
|               <ClearIcon /> |               <ClearIcon /> | ||||||
|             </Button> |             </Button> | ||||||
|           </div> |           </div> | ||||||
|  | @ -307,11 +316,7 @@ export const QuarantineMediaButton = props => { | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <div> |           <div> | ||||||
|             <Button |             <Button onClick={handleRemoveQuarantaine} disabled={loading}> | ||||||
|               {...props} |  | ||||||
|               onClick={handleRemoveQuarantaine} |  | ||||||
|               disabled={isLoading} |  | ||||||
|             > |  | ||||||
|               <BlockIcon color="error" /> |               <BlockIcon color="error" /> | ||||||
|             </Button> |             </Button> | ||||||
|           </div> |           </div> | ||||||
|  | @ -324,12 +329,12 @@ export const QuarantineMediaButton = props => { | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <div> |           <div> | ||||||
|             <Button onClick={handleQuarantaine} disabled={isLoading}> |             <Button onClick={handleQuarantaine} disabled={loading}> | ||||||
|               <BlockIcon /> |               <BlockIcon /> | ||||||
|             </Button> |             </Button> | ||||||
|           </div> |           </div> | ||||||
|         </Tooltip> |         </Tooltip> | ||||||
|       )} |       )} | ||||||
|     </> |     </Fragment> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | @ -1,20 +1,18 @@ | ||||||
| import React from "react"; | import React, { Fragment } from "react"; | ||||||
|  | import { connect } from "react-redux"; | ||||||
| import { | import { | ||||||
|   BooleanField, |   BooleanField, | ||||||
|   BulkDeleteButton, |   BulkDeleteButton, | ||||||
|   DateField, |   DateField, | ||||||
|   Datagrid, |   Datagrid, | ||||||
|   DatagridConfigurable, |  | ||||||
|   DeleteButton, |   DeleteButton, | ||||||
|   ExportButton, |   Filter, | ||||||
|   FunctionField, |  | ||||||
|   List, |   List, | ||||||
|   NumberField, |   NumberField, | ||||||
|   Pagination, |   Pagination, | ||||||
|   ReferenceField, |   ReferenceField, | ||||||
|   ReferenceManyField, |   ReferenceManyField, | ||||||
|   SearchInput, |   SearchInput, | ||||||
|   SelectColumnsButton, |  | ||||||
|   SelectField, |   SelectField, | ||||||
|   Show, |   Show, | ||||||
|   Tab, |   Tab, | ||||||
|  | @ -24,8 +22,10 @@ import { | ||||||
|   useRecordContext, |   useRecordContext, | ||||||
|   useTranslate, |   useTranslate, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import { useTheme } from "@mui/material/styles"; | import get from "lodash/get"; | ||||||
| import Box from "@mui/material/Box"; | 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 FastForwardIcon from "@mui/icons-material/FastForward"; | ||||||
| import HttpsIcon from "@mui/icons-material/Https"; | import HttpsIcon from "@mui/icons-material/Https"; | ||||||
| import NoEncryptionIcon from "@mui/icons-material/NoEncryption"; | 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 ViewListIcon from "@mui/icons-material/ViewList"; | ||||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||||
| import EventIcon from "@mui/icons-material/Event"; | import EventIcon from "@mui/icons-material/Event"; | ||||||
| import RoomIcon from "@mui/icons-material/ViewList"; |  | ||||||
| import { | import { | ||||||
|   RoomDirectoryBulkUnpublishButton, |   RoomDirectoryBulkDeleteButton, | ||||||
|   RoomDirectoryBulkPublishButton, |   RoomDirectoryBulkSaveButton, | ||||||
|   RoomDirectoryUnpublishButton, |   RoomDirectoryDeleteButton, | ||||||
|   RoomDirectoryPublishButton, |   RoomDirectorySaveButton, | ||||||
| } from "./RoomDirectory"; | } from "./RoomDirectory"; | ||||||
| 
 | 
 | ||||||
| const date_format = { | const date_format = { | ||||||
|  | @ -51,11 +50,44 @@ const date_format = { | ||||||
|   second: "2-digit", |   second: "2-digit", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const RoomPagination = () => ( | const useStyles = makeStyles(theme => ({ | ||||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> |   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 record = useRecordContext(); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   var name = ""; |   var name = ""; | ||||||
|  | @ -70,18 +102,24 @@ const RoomTitle = () => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const RoomShowActions = () => { | const RoomShowActions = ({ basePath, data, resource }) => { | ||||||
|   const record = useRecordContext(); |  | ||||||
|   var roomDirectoryStatus = ""; |   var roomDirectoryStatus = ""; | ||||||
|   if (record) { |   if (data) { | ||||||
|     roomDirectoryStatus = record.public; |     roomDirectoryStatus = data.public; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <TopToolbar> |     <TopToolbar> | ||||||
|       {roomDirectoryStatus === false && <RoomDirectoryPublishButton />} |       {roomDirectoryStatus === false && ( | ||||||
|       {roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />} |         <RoomDirectorySaveButton record={data} /> | ||||||
|  |       )} | ||||||
|  |       {roomDirectoryStatus === true && ( | ||||||
|  |         <RoomDirectoryDeleteButton record={data} /> | ||||||
|  |       )} | ||||||
|       <DeleteButton |       <DeleteButton | ||||||
|  |         basePath={basePath} | ||||||
|  |         record={data} | ||||||
|  |         resource={resource} | ||||||
|         mutationMode="pessimistic" |         mutationMode="pessimistic" | ||||||
|         confirmTitle="resources.rooms.action.erase.title" |         confirmTitle="resources.rooms.action.erase.title" | ||||||
|         confirmContent="resources.rooms.action.erase.content" |         confirmContent="resources.rooms.action.erase.content" | ||||||
|  | @ -91,6 +129,7 @@ const RoomShowActions = () => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const RoomShow = props => { | export const RoomShow = props => { | ||||||
|  |   const classes = useStyles({ props }); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   return ( |   return ( | ||||||
|     <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> |     <Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}> | ||||||
|  | @ -98,7 +137,6 @@ export const RoomShow = props => { | ||||||
|         <Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}> |         <Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}> | ||||||
|           <TextField source="room_id" /> |           <TextField source="room_id" /> | ||||||
|           <TextField source="name" /> |           <TextField source="name" /> | ||||||
|           <TextField source="topic" /> |  | ||||||
|           <TextField source="canonical_alias" /> |           <TextField source="canonical_alias" /> | ||||||
|           <ReferenceField source="creator" reference="users"> |           <ReferenceField source="creator" reference="users"> | ||||||
|             <TextField source="id" /> |             <TextField source="id" /> | ||||||
|  | @ -133,8 +171,7 @@ export const RoomShow = props => { | ||||||
|           > |           > | ||||||
|             <Datagrid |             <Datagrid | ||||||
|               style={{ width: "100%" }} |               style={{ width: "100%" }} | ||||||
|               rowClick={(id, resource, record) => "/users/" + id} |               rowClick={(id, basePath, record) => "/users/" + id} | ||||||
|               bulkActionButtons={false} |  | ||||||
|             > |             > | ||||||
|               <TextField |               <TextField | ||||||
|                 source="id" |                 source="id" | ||||||
|  | @ -219,7 +256,7 @@ export const RoomShow = props => { | ||||||
|             target="room_id" |             target="room_id" | ||||||
|             addLabel={false} |             addLabel={false} | ||||||
|           > |           > | ||||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> |             <Datagrid style={{ width: "100%" }}> | ||||||
|               <TextField source="type" sortable={false} /> |               <TextField source="type" sortable={false} /> | ||||||
|               <DateField |               <DateField | ||||||
|                 source="origin_server_ts" |                 source="origin_server_ts" | ||||||
|  | @ -244,20 +281,15 @@ export const RoomShow = props => { | ||||||
|           icon={<FastForwardIcon />} |           icon={<FastForwardIcon />} | ||||||
|           path="forward_extremities" |           path="forward_extremities" | ||||||
|         > |         > | ||||||
|           <Box |           <div className={classes.helper_forward_extremities}> | ||||||
|             sx={{ |  | ||||||
|               fontFamily: "Roboto, Helvetica, Arial, sans-serif", |  | ||||||
|               margin: "0.5em", |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             {translate("resources.rooms.helper.forward_extremities")} |             {translate("resources.rooms.helper.forward_extremities")} | ||||||
|           </Box> |           </div> | ||||||
|           <ReferenceManyField |           <ReferenceManyField | ||||||
|             reference="forward_extremities" |             reference="forward_extremities" | ||||||
|             target="room_id" |             target="room_id" | ||||||
|             addLabel={false} |             addLabel={false} | ||||||
|           > |           > | ||||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> |             <Datagrid style={{ width: "100%" }}> | ||||||
|               <TextField source="id" sortable={false} /> |               <TextField source="id" sortable={false} /> | ||||||
|               <DateField |               <DateField | ||||||
|                 source="received_ts" |                 source="received_ts" | ||||||
|  | @ -275,81 +307,104 @@ export const RoomShow = props => { | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const RoomBulkActionButtons = () => ( | const RoomBulkActionButtons = props => ( | ||||||
|   <> |   <Fragment> | ||||||
|     <RoomDirectoryBulkPublishButton /> |     <RoomDirectoryBulkSaveButton {...props} /> | ||||||
|     <RoomDirectoryBulkUnpublishButton /> |     <RoomDirectoryBulkDeleteButton {...props} /> | ||||||
|     <BulkDeleteButton |     <BulkDeleteButton | ||||||
|  |       {...props} | ||||||
|       confirmTitle="resources.rooms.action.erase.title" |       confirmTitle="resources.rooms.action.erase.title" | ||||||
|       confirmContent="resources.rooms.action.erase.content" |       confirmContent="resources.rooms.action.erase.content" | ||||||
|       mutationMode="pessimistic" |       mutationMode="pessimistic" | ||||||
|     /> |     /> | ||||||
|   </> |   </Fragment> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const roomFilters = [<SearchInput source="search_term" alwaysOn />]; | const RoomFilter = ({ ...props }) => { | ||||||
| 
 |   const translate = useTranslate(); | ||||||
| const RoomListActions = () => ( |   return ( | ||||||
|   <TopToolbar> |     <Filter {...props}> | ||||||
|     <SelectColumnsButton /> |       <SearchInput source="search_term" alwaysOn /> | ||||||
|     <ExportButton /> |       <Chip | ||||||
|   </TopToolbar> |         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> | ||||||
|   ); |   ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export const RoomList = props => { | const RoomNameField = props => { | ||||||
|   const theme = useTheme(); |   const { source } = props; | ||||||
|  |   const record = useRecordContext(); | ||||||
|  |   return ( | ||||||
|  |     <span>{record[source] || record["canonical_alias"] || record["id"]}</span> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 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 ( |   return ( | ||||||
|     <List |     <List | ||||||
|       {...props} |       {...props} | ||||||
|       pagination={<RoomPagination />} |       pagination={<RoomPagination />} | ||||||
|       sort={{ field: "name", order: "ASC" }} |       sort={{ field: "name", order: "ASC" }} | ||||||
|       filters={roomFilters} |       filters={<RoomFilter />} | ||||||
|       actions={<RoomListActions />} |  | ||||||
|     > |  | ||||||
|       <DatagridConfigurable |  | ||||||
|         rowClick="show" |  | ||||||
|       bulkActionButtons={<RoomBulkActionButtons />} |       bulkActionButtons={<RoomBulkActionButtons />} | ||||||
|         omit={[ |  | ||||||
|           "joined_local_members", |  | ||||||
|           "state_events", |  | ||||||
|           "version", |  | ||||||
|           "federatable", |  | ||||||
|         ]} |  | ||||||
|     > |     > | ||||||
|         <BooleanField |       <Datagrid rowClick="show"> | ||||||
|  |         <EncryptionField | ||||||
|           source="is_encrypted" |           source="is_encrypted" | ||||||
|           sortBy="encryption" |           sortBy="encryption" | ||||||
|           TrueIcon={HttpsIcon} |  | ||||||
|           FalseIcon={NoEncryptionIcon} |  | ||||||
|           label={<HttpsIcon />} |           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_members" /> | ||||||
|         <TextField source="joined_local_members" /> |         {localMembersFilter && <TextField source="joined_local_members" />} | ||||||
|         <TextField source="state_events" /> |         {stateEventsFilter && <TextField source="state_events" />} | ||||||
|         <TextField source="version" /> |         {versionFilter && <TextField source="version" />} | ||||||
|         <BooleanField source="federatable" /> |         {federateableFilter && <BooleanField source="federatable" />} | ||||||
|         <BooleanField source="public" /> |         <BooleanField source="public" /> | ||||||
|       </DatagridConfigurable> |       </Datagrid> | ||||||
|     </List> |     </List> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const resource = { | function mapStateToProps(state) { | ||||||
|   name: "rooms", |   return { | ||||||
|   icon: RoomIcon, |     roomFilters: state.admin.resources.rooms.list.params.displayedFilters, | ||||||
|   list: RoomList, |  | ||||||
|   show: RoomShow, |  | ||||||
|   }; |   }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 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 AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; | ||||||
| import ContactMailIcon from "@mui/icons-material/ContactMail"; | import ContactMailIcon from "@mui/icons-material/ContactMail"; | ||||||
| import DevicesIcon from "@mui/icons-material/Devices"; | 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 PermMediaIcon from "@mui/icons-material/PermMedia"; | ||||||
| import PersonPinIcon from "@mui/icons-material/PersonPin"; | import PersonPinIcon from "@mui/icons-material/PersonPin"; | ||||||
| import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; | import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; | ||||||
| import UserIcon from "@mui/icons-material/Group"; |  | ||||||
| import ViewListIcon from "@mui/icons-material/ViewList"; | import ViewListIcon from "@mui/icons-material/ViewList"; | ||||||
| import { | import { | ||||||
|   ArrayInput, |   ArrayInput, | ||||||
|  | @ -18,6 +18,7 @@ import { | ||||||
|   Create, |   Create, | ||||||
|   Edit, |   Edit, | ||||||
|   List, |   List, | ||||||
|  |   Filter, | ||||||
|   Toolbar, |   Toolbar, | ||||||
|   SimpleForm, |   SimpleForm, | ||||||
|   SimpleFormIterator, |   SimpleFormIterator, | ||||||
|  | @ -48,10 +49,28 @@ import { | ||||||
|   NumberField, |   NumberField, | ||||||
| } from "react-admin"; | } from "react-admin"; | ||||||
| import { Link } from "react-router-dom"; | import { Link } from "react-router-dom"; | ||||||
| import AvatarField from "./AvatarField"; |  | ||||||
| import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; | import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; | ||||||
| import { DeviceRemoveButton } from "./devices"; | import { DeviceRemoveButton } from "./devices"; | ||||||
| import { ProtectMediaButton, QuarantineMediaButton } from "./media"; | 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 = [ | const choices_medium = [ | ||||||
|   { id: "email", name: "resources.users.email" }, |   { id: "email", name: "resources.users.email" }, | ||||||
|  | @ -73,7 +92,7 @@ const date_format = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const UserListActions = ({ | const UserListActions = ({ | ||||||
|   sort, |   currentSort, | ||||||
|   className, |   className, | ||||||
|   resource, |   resource, | ||||||
|   filters, |   filters, | ||||||
|  | @ -82,6 +101,7 @@ const UserListActions = ({ | ||||||
|   filterValues, |   filterValues, | ||||||
|   permanentFilter, |   permanentFilter, | ||||||
|   hasCreate, // you can hide CreateButton if hasCreate = false
 |   hasCreate, // you can hide CreateButton if hasCreate = false
 | ||||||
|  |   basePath, | ||||||
|   selectedIds, |   selectedIds, | ||||||
|   onUnselectItems, |   onUnselectItems, | ||||||
|   showFilter, |   showFilter, | ||||||
|  | @ -99,18 +119,18 @@ const UserListActions = ({ | ||||||
|           filterValues, |           filterValues, | ||||||
|           context: "button", |           context: "button", | ||||||
|         })} |         })} | ||||||
|       <CreateButton /> |       <CreateButton basePath={basePath} /> | ||||||
|       <ExportButton |       <ExportButton | ||||||
|         disabled={total === 0} |         disabled={total === 0} | ||||||
|         resource={resource} |         resource={resource} | ||||||
|         sort={sort} |         sort={currentSort} | ||||||
|         filter={{ ...filterValues, ...permanentFilter }} |         filter={{ ...filterValues, ...permanentFilter }} | ||||||
|         exporter={exporter} |         exporter={exporter} | ||||||
|         maxResults={maxResults} |         maxResults={maxResults} | ||||||
|       /> |       /> | ||||||
|       {/* Add your custom actions */} |       {/* Add your custom actions */} | ||||||
|       <Button component={Link} to="/import_users" label="CSV Import"> |       <Button component={Link} to={redirect} label="CSV Import"> | ||||||
|         <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} /> |         <GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> | ||||||
|       </Button> |       </Button> | ||||||
|     </TopToolbar> |     </TopToolbar> | ||||||
|   ); |   ); | ||||||
|  | @ -121,44 +141,54 @@ UserListActions.defaultProps = { | ||||||
|   onUnselectItems: () => null, |   onUnselectItems: () => null, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const UserPagination = () => ( | const UserPagination = props => ( | ||||||
|   <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> |   <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const userFilters = [ | const UserFilter = props => ( | ||||||
|   <SearchInput source="name" alwaysOn />, |   <Filter {...props}> | ||||||
|   <BooleanInput source="guests" alwaysOn />, |     <SearchInput source="name" alwaysOn /> | ||||||
|  |     <BooleanInput source="guests" alwaysOn /> | ||||||
|     <BooleanInput |     <BooleanInput | ||||||
|       label="resources.users.fields.show_deactivated" |       label="resources.users.fields.show_deactivated" | ||||||
|       source="deactivated" |       source="deactivated" | ||||||
|       alwaysOn |       alwaysOn | ||||||
|   />, |     /> | ||||||
| ]; |   </Filter> | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| const UserBulkActionButtons = () => ( | const UserBulkActionButtons = props => ( | ||||||
|   <> |   <Fragment> | ||||||
|     <ServerNoticeBulkButton /> |     <ServerNoticeBulkButton {...props} /> | ||||||
|     <BulkDeleteButton |     <BulkDeleteButton | ||||||
|  |       {...props} | ||||||
|       label="resources.users.action.erase" |       label="resources.users.action.erase" | ||||||
|       confirmTitle="resources.users.helper.erase" |       confirmTitle="resources.users.helper.erase" | ||||||
|       mutationMode="pessimistic" |       mutationMode="pessimistic" | ||||||
|     /> |     /> | ||||||
|   </> |   </Fragment> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const UserList = props => ( | const AvatarField = ({ source, className, record = {} }) => ( | ||||||
|  |   <Avatar src={record[source]} className={className} /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const UserList = props => { | ||||||
|  |   const classes = useStyles(); | ||||||
|  |   return ( | ||||||
|     <List |     <List | ||||||
|       {...props} |       {...props} | ||||||
|     filters={userFilters} |       filters={<UserFilter />} | ||||||
|       filterDefaultValues={{ guests: true, deactivated: false }} |       filterDefaultValues={{ guests: true, deactivated: false }} | ||||||
|       sort={{ field: "name", order: "ASC" }} |       sort={{ field: "name", order: "ASC" }} | ||||||
|       actions={<UserListActions maxResults={10000} />} |       actions={<UserListActions maxResults={10000} />} | ||||||
|  |       bulkActionButtons={<UserBulkActionButtons />} | ||||||
|       pagination={<UserPagination />} |       pagination={<UserPagination />} | ||||||
|     > |     > | ||||||
|     <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}> |       <Datagrid rowClick="edit"> | ||||||
|         <AvatarField |         <AvatarField | ||||||
|           source="avatar_src" |           source="avatar_src" | ||||||
|         sx={{ height: "40px", width: "40px" }} |           className={classes.small} | ||||||
|           sortBy="avatar_url" |           sortBy="avatar_url" | ||||||
|         /> |         /> | ||||||
|         <TextField source="id" sortBy="name" /> |         <TextField source="id" sortBy="name" /> | ||||||
|  | @ -175,6 +205,7 @@ export const UserList = props => ( | ||||||
|       </Datagrid> |       </Datagrid> | ||||||
|     </List> |     </List> | ||||||
|   ); |   ); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| // https://matrix.org/docs/spec/appendices#user-identifiers
 | // https://matrix.org/docs/spec/appendices#user-identifiers
 | ||||||
| // here only local part of user_id
 | // here only local part of user_id
 | ||||||
|  | @ -231,7 +262,7 @@ export function generateRandomUser() { | ||||||
| 
 | 
 | ||||||
| const UserEditToolbar = props => ( | const UserEditToolbar = props => ( | ||||||
|   <Toolbar {...props}> |   <Toolbar {...props}> | ||||||
|     <SaveButton disabled={props.pristine} /> |     <SaveButton submitOnEnter={true} disabled={props.pristine} /> | ||||||
|   </Toolbar> |   </Toolbar> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -271,6 +302,7 @@ export const UserCreate = props => ( | ||||||
|         source="user_type" |         source="user_type" | ||||||
|         choices={choices_type} |         choices={choices_type} | ||||||
|         translateChoice={false} |         translateChoice={false} | ||||||
|  |         allowEmpty={true} | ||||||
|         resettable |         resettable | ||||||
|       /> |       /> | ||||||
|       <BooleanInput source="admin" /> |       <BooleanInput source="admin" /> | ||||||
|  | @ -298,7 +330,7 @@ export const UserCreate = props => ( | ||||||
|   </Create> |   </Create> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const UserTitle = () => { | const UserTitle = props => { | ||||||
|   const record = useRecordContext(); |   const record = useRecordContext(); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   return ( |   return ( | ||||||
|  | @ -312,6 +344,7 @@ const UserTitle = () => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const UserEdit = props => { | export const UserEdit = props => { | ||||||
|  |   const classes = useStyles(); | ||||||
|   const translate = useTranslate(); |   const translate = useTranslate(); | ||||||
|   return ( |   return ( | ||||||
|     <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> |     <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> | ||||||
|  | @ -323,7 +356,7 @@ export const UserEdit = props => { | ||||||
|           <AvatarField |           <AvatarField | ||||||
|             source="avatar_src" |             source="avatar_src" | ||||||
|             sortable={false} |             sortable={false} | ||||||
|             sx={{ height: "120px", width: "120px", float: "right" }} |             className={classes.large} | ||||||
|           /> |           /> | ||||||
|           <TextInput source="id" disabled /> |           <TextInput source="id" disabled /> | ||||||
|           <TextInput source="displayname" /> |           <TextInput source="displayname" /> | ||||||
|  | @ -336,6 +369,7 @@ export const UserEdit = props => { | ||||||
|             source="user_type" |             source="user_type" | ||||||
|             choices={choices_type} |             choices={choices_type} | ||||||
|             translateChoice={false} |             translateChoice={false} | ||||||
|  |             allowEmpty={true} | ||||||
|             resettable |             resettable | ||||||
|           /> |           /> | ||||||
|           <BooleanInput source="admin" /> |           <BooleanInput source="admin" /> | ||||||
|  | @ -417,7 +451,7 @@ export const UserEdit = props => { | ||||||
|               source="devices[].sessions[0].connections" |               source="devices[].sessions[0].connections" | ||||||
|               label="resources.connections.name" |               label="resources.connections.name" | ||||||
|             > |             > | ||||||
|               <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> |               <Datagrid style={{ width: "100%" }}> | ||||||
|                 <TextField source="ip" sortable={false} /> |                 <TextField source="ip" sortable={false} /> | ||||||
|                 <DateField |                 <DateField | ||||||
|                   source="last_seen" |                   source="last_seen" | ||||||
|  | @ -479,8 +513,7 @@ export const UserEdit = props => { | ||||||
|           > |           > | ||||||
|             <Datagrid |             <Datagrid | ||||||
|               style={{ width: "100%" }} |               style={{ width: "100%" }} | ||||||
|               rowClick={(id, resource, record) => "/rooms/" + id + "/show"} |               rowClick={(id, basePath, record) => "/rooms/" + id + "/show"} | ||||||
|               bulkActionButtons={false} |  | ||||||
|             > |             > | ||||||
|               <TextField |               <TextField | ||||||
|                 source="id" |                 source="id" | ||||||
|  | @ -510,7 +543,7 @@ export const UserEdit = props => { | ||||||
|             target="user_id" |             target="user_id" | ||||||
|             addLabel={false} |             addLabel={false} | ||||||
|           > |           > | ||||||
|             <Datagrid style={{ width: "100%" }} bulkActionButtons={false}> |             <Datagrid style={{ width: "100%" }}> | ||||||
|               <TextField source="kind" sortable={false} /> |               <TextField source="kind" sortable={false} /> | ||||||
|               <TextField source="app_display_name" sortable={false} /> |               <TextField source="app_display_name" sortable={false} /> | ||||||
|               <TextField source="app_id" sortable={false} /> |               <TextField source="app_id" sortable={false} /> | ||||||
|  | @ -526,13 +559,3 @@ export const UserEdit = props => { | ||||||
|     </Edit> |     </Edit> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| const resource = { |  | ||||||
|   name: "users", |  | ||||||
|   icon: UserIcon, |  | ||||||
|   list: UserList, |  | ||||||
|   edit: UserEdit, |  | ||||||
|   create: UserCreate, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default resource; |  | ||||||
|  | @ -188,7 +188,7 @@ const de = { | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     reports: { |     reports: { | ||||||
|       name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse", |       name: "Ereignisbericht |||| Ereignisberichte", | ||||||
|       fields: { |       fields: { | ||||||
|         id: "ID", |         id: "ID", | ||||||
|         received_ts: "Meldezeit", |         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: { |     connections: { | ||||||
|       name: "Verbindungen", |       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: { |     connections: { | ||||||
|       name: "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 => ({ |     delete: params => ({ | ||||||
|       endpoint: `/_synapse/admin/v2/users/${encodeURIComponent( |       endpoint: `/_synapse/admin/v2/users/${encodeURIComponent( | ||||||
|         params.previousData.user_id |         params.user_id | ||||||
|       )}/devices/${params.id}`,
 |       )}/devices/${params.id}`,
 | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|  | @ -184,9 +184,9 @@ const resourceMap = { | ||||||
|     delete: params => ({ |     delete: params => ({ | ||||||
|       endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( |       endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( | ||||||
|         "home_server" |         "home_server" | ||||||
|       )}/delete?before_ts=${params.meta.before_ts}&size_gt=${ |       )}/delete?before_ts=${params.before_ts}&size_gt=${ | ||||||
|         params.meta.size_gt |         params.size_gt | ||||||
|       }&keep_profiles=${params.meta.keep_profiles}`,
 |       }&keep_profiles=${params.keep_profiles}`,
 | ||||||
|       method: "POST", |       method: "POST", | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|  | @ -197,7 +197,7 @@ const resourceMap = { | ||||||
|       method: "POST", |       method: "POST", | ||||||
|     }), |     }), | ||||||
|     delete: params => ({ |     delete: params => ({ | ||||||
|       endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`, |       endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`, | ||||||
|       method: "POST", |       method: "POST", | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|  | @ -212,7 +212,7 @@ const resourceMap = { | ||||||
|     delete: params => ({ |     delete: params => ({ | ||||||
|       endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem( |       endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem( | ||||||
|         "home_server" |         "home_server" | ||||||
|       )}/${params.id}`,
 |       )}/${params.media_id}`,
 | ||||||
|       method: "POST", |       method: "POST", | ||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|  | @ -456,7 +456,7 @@ const dataProvider = { | ||||||
|     const res = resourceMap[resource]; |     const res = resourceMap[resource]; | ||||||
| 
 | 
 | ||||||
|     const endpoint_url = homeserver + res.path; |     const endpoint_url = homeserver + res.path; | ||||||
|     return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, { |     return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, { | ||||||
|       method: "PUT", |       method: "PUT", | ||||||
|       body: JSON.stringify(params.data, filterNullValues), |       body: JSON.stringify(params.data, filterNullValues), | ||||||
|     }).then(({ json }) => ({ |     }).then(({ json }) => ({ | ||||||
|  | @ -546,7 +546,7 @@ const dataProvider = { | ||||||
|       const endpoint_url = homeserver + res.path; |       const endpoint_url = homeserver + res.path; | ||||||
|       return jsonClient(`${endpoint_url}/${params.id}`, { |       return jsonClient(`${endpoint_url}/${params.id}`, { | ||||||
|         method: "DELETE", |         method: "DELETE", | ||||||
|         body: JSON.stringify(params.previousData, filterNullValues), |         body: JSON.stringify(params.data, filterNullValues), | ||||||
|       }).then(({ json }) => ({ |       }).then(({ json }) => ({ | ||||||
|         data: 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