mirror of
https://github.com/UA-Fediland/synapse-admin.git
synced 2024-11-23 15:01:28 +00:00
Compare commits
33 commits
7deb9bcf7e
...
b112689b8c
Author | SHA1 | Date | |
---|---|---|---|
|
b112689b8c | ||
|
ac3b40b188 | ||
|
a490b7bc85 | ||
|
e094047388 | ||
|
5d1e43611c | ||
|
630e809e78 | ||
|
264e0b5ec6 | ||
|
1837733e07 | ||
|
77be88402f | ||
|
f4ea63c8f4 | ||
|
2e2085cdfe | ||
|
4b1277f653 | ||
|
ef3836313c | ||
|
428cb56fdf | ||
|
c6f9dbec18 | ||
|
531d8f2d7f | ||
|
0986d95de2 | ||
|
5f1dfc95c7 | ||
|
e666c9c7bd | ||
|
441f7749a2 | ||
|
028babc885 | ||
|
400e9aa416 | ||
|
6d9abe85b0 | ||
|
df1fbbc16b | ||
|
6bdeadcc3e | ||
|
881760c8d8 | ||
|
03c4955ef7 | ||
|
c9cb9aa9e0 | ||
|
25020c2d5b | ||
|
1acffdb618 | ||
|
0b4f3a60c0 | ||
|
33d29e01b1 | ||
|
a2e47cb793 |
55 changed files with 11303 additions and 10880 deletions
|
@ -1,10 +1,13 @@
|
|||
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
||||
tests/
|
||||
build/
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
electron_app/
|
||||
karma-reports/
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.idea/
|
||||
.tmp/
|
||||
config.json*
|
||||
|
|
5
.env
5
.env
|
@ -1,5 +0,0 @@
|
|||
# This setting allows to fix the homeserver.
|
||||
# If you set this setting, the user will not be able to select
|
||||
# the server and have to use synapse-admin with this server.
|
||||
|
||||
#REACT_APP_SERVER=https://yourmatrixserver.example.com
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
yarn*.cjs binary
|
46
.github/workflows/docker-release.yml
vendored
46
.github/workflows/docker-release.yml
vendored
|
@ -1,4 +1,5 @@
|
|||
name: Create docker image(s) and push to docker hub
|
||||
name: Create docker image(s) and push to docker hub and ghcr.io
|
||||
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -13,39 +14,50 @@ on:
|
|||
|
||||
jobs:
|
||||
docker:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Calculate docker image tag
|
||||
id: set-tag
|
||||
run: |
|
||||
case "${GITHUB_REF}" in
|
||||
refs/heads/master|refs/heads/main)
|
||||
tag=latest
|
||||
;;
|
||||
refs/tags/*)
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
;;
|
||||
*)
|
||||
tag=${GITHUB_SHA}
|
||||
;;
|
||||
esac
|
||||
echo "::set-output name=tag::$tag"
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
awesometechnologies/synapse-admin
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and Push Tag
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
11
.github/workflows/edge_ghpage.yml
vendored
11
.github/workflows/edge_ghpage.yml
vendored
|
@ -11,16 +11,19 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
fetch-tags: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "20"
|
||||
- name: Install and Build 🔧
|
||||
run: |
|
||||
yarn install --immutable
|
||||
yarn build
|
||||
yarn build --base=/synapse-admin
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
||||
uses: JamesIves/github-pages-deploy-action@v4.6.0
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: build
|
||||
folder: dist
|
||||
|
|
7
.github/workflows/github-release.yml
vendored
7
.github/workflows/github-release.yml
vendored
|
@ -16,15 +16,14 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "20"
|
||||
- run: yarn install --immutable
|
||||
- run: yarn build
|
||||
- run: |
|
||||
version=`git describe --dirty --tags || echo unknown`
|
||||
mkdir -p dist
|
||||
cp -r build synapse-admin-$version
|
||||
cp -r dist synapse-admin-$version
|
||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||
- uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52
|
||||
- uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
|
||||
with:
|
||||
files: dist/*.tar.gz
|
||||
env:
|
||||
|
|
51
.github/workflows/test-docker-image.yml
vendored
51
.github/workflows/test-docker-image.yml
vendored
|
@ -1,51 +0,0 @@
|
|||
name: Test docker image creation
|
||||
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/heads
|
||||
# prettier-ignore
|
||||
branches:
|
||||
# Push events on branch fix_docker_cd
|
||||
- fix_docker_cd
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Calculate docker image tag
|
||||
id: set-tag
|
||||
run: |
|
||||
case "${GITHUB_REF}" in
|
||||
refs/heads/master|refs/heads/main)
|
||||
tag=latest
|
||||
;;
|
||||
refs/tags/*)
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
;;
|
||||
*)
|
||||
tag=${GITHUB_SHA}
|
||||
;;
|
||||
esac
|
||||
echo "::set-output name=tag::$tag"
|
||||
- name: Build and Push Tag
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
||||
platforms: linux/amd64,linux/arm64
|
208
.gitignore
vendored
208
.gitignore
vendored
|
@ -1,23 +1,193 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,react,visualstudiocode
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### react ###
|
||||
.DS_*
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
|
||||
*.sublime*
|
||||
|
||||
psd
|
||||
thumb
|
||||
sketch
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### yarn ###
|
||||
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# if you are NOT using Zero-installs, then:
|
||||
# comment the following lines
|
||||
!.yarn/cache
|
||||
|
||||
# and uncomment the following lines
|
||||
# .pnp.*
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
||||
|
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
.yarn
|
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
"**/.pnp.*": true
|
||||
},
|
||||
"eslint.nodePath": ".yarn/sdks",
|
||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
BIN
.yarn/releases/yarn-4.1.1.cjs
vendored
Executable file
BIN
.yarn/releases/yarn-4.1.1.cjs
vendored
Executable file
Binary file not shown.
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/bin/eslint.js your application uses
|
||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = absRequire(`eslint`);
|
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/use-at-your-own-risk
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/use-at-your-own-risk your application uses
|
||||
module.exports = absRequire(`eslint/use-at-your-own-risk`);
|
14
.yarn/sdks/eslint/package.json
vendored
Normal file
14
.yarn/sdks/eslint/package.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "eslint",
|
||||
"version": "8.57.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"eslint": "./bin/eslint.js"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./lib/api.js",
|
||||
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
||||
}
|
||||
}
|
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
||||
integrations:
|
||||
- vscode
|
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier/bin/prettier.cjs
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier/bin/prettier.cjs your application uses
|
||||
module.exports = absRequire(`prettier/bin/prettier.cjs`);
|
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier your application uses
|
||||
module.exports = absRequire(`prettier`);
|
7
.yarn/sdks/prettier/package.json
vendored
Normal file
7
.yarn/sdks/prettier/package.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "prettier",
|
||||
"version": "3.2.5-sdk",
|
||||
"main": "./index.cjs",
|
||||
"type": "commonjs",
|
||||
"bin": "./bin/prettier.cjs"
|
||||
}
|
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsc
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsc your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsc`);
|
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsserver
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsserver your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
225
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
225
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
225
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
225
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript your application uses
|
||||
module.exports = absRequire(`typescript`);
|
10
.yarn/sdks/typescript/package.json
vendored
Normal file
10
.yarn/sdks/typescript/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "typescript",
|
||||
"version": "5.4.5-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"tsc": "./bin/tsc",
|
||||
"tsserver": "./bin/tsserver"
|
||||
}
|
||||
}
|
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
|||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
21
Dockerfile
21
Dockerfile
|
@ -1,19 +1,26 @@
|
|||
# Builder
|
||||
FROM node:lts as builder
|
||||
|
||||
ARG REACT_APP_SERVER
|
||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||
# Base path for synapse admin
|
||||
ARG BASE_PATH=./
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src
|
||||
RUN yarn --network-timeout=300000 install --immutable
|
||||
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||
# Copy .yarn directory to the working directory (must be on a separate line!)
|
||||
# Use https://docs.docker.com/engine/reference/builder/#copy---parents when available
|
||||
COPY .yarn .yarn
|
||||
COPY package.json .yarnrc.yml yarn.lock ./
|
||||
|
||||
# Disable telemetry and install packages
|
||||
RUN yarn config set enableTelemetry 0 && yarn install --immutable --network-timeout=300000
|
||||
|
||||
COPY . /src
|
||||
RUN yarn build --base=$BASE_PATH
|
||||
|
||||
# App
|
||||
FROM nginx:alpine
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY --from=builder /src/build /app
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
|
|
86
README.md
86
README.md
|
@ -64,11 +64,6 @@ You have three options:
|
|||
- download dependencies: `yarn install`
|
||||
- start web server: `yarn start`
|
||||
|
||||
You can fix the homeserver, so that the user can no longer define it himself.
|
||||
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
|
||||
or by editing it in the [.env](.env) file. See also the
|
||||
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
||||
|
||||
#### Steps for 3)
|
||||
|
||||
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
||||
|
@ -76,19 +71,16 @@ or by editing it in the [.env](.env) file. See also the
|
|||
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
hostname: synapse-admin
|
||||
build:
|
||||
context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||
# args:
|
||||
args:
|
||||
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# # see #266, PUBLIC_URL must be without surrounding quotation marks
|
||||
# - PUBLIC_URL=/synapse-admin
|
||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
||||
# - BASE_PATH="/synapse-admin"
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
@ -96,11 +88,83 @@ or by editing it in the [.env](.env) file. See also the
|
|||
|
||||
- browse to http://localhost:8080
|
||||
|
||||
### Restricting available homeserver
|
||||
|
||||
You can restrict the homeserver(s), so that the user can no longer define it himself.
|
||||
|
||||
Edit `config.json` to restrict either to a single homeserver:
|
||||
|
||||
```json
|
||||
{
|
||||
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
or to a list of homeservers:
|
||||
|
||||
```json
|
||||
{
|
||||
"restrictBaseUrl": [
|
||||
"https://your-first-matrix-server.example.com",
|
||||
"https://your-second-matrix-server.example.com"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `config.json` can be injected into a Docker container using a bind mount.
|
||||
|
||||
```yml
|
||||
services:
|
||||
synapse-admin:
|
||||
...
|
||||
volumes:
|
||||
./config.json:/app/config.json:ro
|
||||
...
|
||||
```
|
||||
|
||||
### Serving Synapse-Admin on a different path
|
||||
|
||||
The path prefix where synapse-admin is served can only be changed during the build step.
|
||||
|
||||
If you downloaded the source code, use `yarn build --base=/my-prefix` to set a path prefix.
|
||||
|
||||
If you want to build your own Docker container, use the `BASE_PATH` argument.
|
||||
|
||||
We do not support directly changing the path where Synapse-Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse-Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
|
||||
|
||||
Example for Traefik:
|
||||
|
||||
`docker-compose.yml`
|
||||
|
||||
```yml
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:mimolette
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
synapse-admin:
|
||||
image: awesometechnologies/synapse-admin:latest
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
|
||||
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
|
||||
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
|
||||
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
|
||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Screenshots](./screenshots.jpg)
|
||||
|
||||
## Development
|
||||
|
||||
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
||||
- Use `yarn test` to run all style, lint and unit tests
|
||||
- Use `yarn fix` to fix the coding style
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
hostname: synapse-admin
|
||||
image: awesometechnologies/synapse-admin:latest
|
||||
# build:
|
||||
# context: .
|
||||
# context: .
|
||||
|
||||
# to use the docker-compose as standalone without a local repo clone,
|
||||
# replace the context definition with this:
|
||||
# context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||
|
||||
# args:
|
||||
# if you're building on an architecture other than amd64, make sure
|
||||
# to define a maximum ram for node. otherwise the build will fail.
|
||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# default is .
|
||||
# - PUBLIC_URL=/synapse-admin
|
||||
# You can use a fixed homeserver, so that the user can no longer
|
||||
# define it himself
|
||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
||||
# args:
|
||||
# - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
# if you're building on an architecture other than amd64, make sure
|
||||
# to define a maximum ram for node. otherwise the build will fail.
|
||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# - BASE_PATH="/synapse-admin"
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
|
132
index.html
Normal file
132
index.html
Normal file
|
@ -0,0 +1,132 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Synapse-Admin"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="shortcut icon" href="./favicon.ico" />
|
||||
<title>Synapse-Admin</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
|
||||
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: #283593;
|
||||
font-size: 11px;
|
||||
text-indent: -99999em;
|
||||
margin: 55px auto;
|
||||
position: relative;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
box-shadow: inset 0 0 0 1em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
width: 5.2em;
|
||||
height: 10.2em;
|
||||
background: #fafafa;
|
||||
border-radius: 10.2em 0 0 10.2em;
|
||||
top: -0.1em;
|
||||
left: -0.1em;
|
||||
-webkit-transform-origin: 5.2em 5.1em;
|
||||
transform-origin: 5.2em 5.1em;
|
||||
-webkit-animation: load2 2s infinite ease 1.5s;
|
||||
animation: load2 2s infinite ease 1.5s;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
width: 5.2em;
|
||||
height: 10.2em;
|
||||
background: #fafafa;
|
||||
border-radius: 0 10.2em 10.2em 0;
|
||||
top: -0.1em;
|
||||
left: 5.1em;
|
||||
-webkit-transform-origin: 0px 5.1em;
|
||||
transform-origin: 0px 5.1em;
|
||||
-webkit-animation: load2 2s infinite ease;
|
||||
animation: load2 2s infinite ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<div class="loader-container">
|
||||
<div class="loader">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
<footer
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
<script>document.getElementById("version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
|
||||
</html>
|
70
package.json
70
package.json
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"name": "synapse-admin",
|
||||
"version": "0.9.2",
|
||||
"version": "0.10.1",
|
||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||
"type": "module",
|
||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": ".",
|
||||
|
@ -9,48 +10,87 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"prettier": "^3.2.5"
|
||||
"prettier": "^3.2.5",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-version-mark": "^0.0.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^5.15.15",
|
||||
"@mui/material": "^5.15.15",
|
||||
"@mui/styles": "^5.15.15",
|
||||
"history": "^5.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"ra-language-chinese": "^2.0.10",
|
||||
"ra-language-french": "^4.16.15",
|
||||
"ra-language-german": "^3.13.4",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"query-string": "^7.1.1",
|
||||
"ra-core": "^4.16.15",
|
||||
"ra-i18n-polyglot": "^4.16.15",
|
||||
"ra-language-english": "^4.16.15",
|
||||
"ra-language-farsi": "^4.2.0",
|
||||
"ra-language-french": "^4.16.15",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"react": "^18.0.0",
|
||||
"react-admin": "^4.16.15",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-is": "^18.2.0",
|
||||
"react-query": "^3.32.1",
|
||||
"react-router": "^6.1.0",
|
||||
"react-router-dom": "^6.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
||||
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
|
||||
"start": "vite serve",
|
||||
"build": "vite build",
|
||||
"fix:other": "yarn prettier --write",
|
||||
"fix:code": "yarn test:lint --fix",
|
||||
"fix": "yarn fix:code && yarn fix:other",
|
||||
"prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
|
||||
"test:code": "react-scripts test",
|
||||
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
|
||||
"test:code": "jest",
|
||||
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
|
||||
"test:style": "yarn prettier --list-different",
|
||||
"test": "yarn test:style && yarn test:lint && yarn test:code",
|
||||
"eject": "react-scripts eject"
|
||||
"test:style": "yarn prettier --check",
|
||||
"test": "yarn test:style && yarn test:lint && yarn test:code"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupTests.js"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
|
1
public/config.json
Normal file
1
public/config.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1,49 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Synapse-Admin"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Synapse-Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<footer
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||
Synapse-Admin <b>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
14
src/App.jsx
14
src/App.jsx
|
@ -6,6 +6,7 @@ import {
|
|||
resolveBrowserLocale,
|
||||
} from "react-admin";
|
||||
import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||
import merge from "lodash/merge";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
import users from "./components/users";
|
||||
|
@ -33,8 +34,17 @@ const messages = {
|
|||
zh: chineseMessages,
|
||||
};
|
||||
const i18nProvider = polyglotI18nProvider(
|
||||
locale => (messages[locale] ? messages[locale] : messages.en),
|
||||
resolveBrowserLocale()
|
||||
locale =>
|
||||
messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en,
|
||||
resolveBrowserLocale(),
|
||||
[
|
||||
{ locale: "en", name: "English" },
|
||||
{ locale: "de", name: "Deutsch" },
|
||||
{ locale: "fr", name: "Français" },
|
||||
{ locale: "it", name: "Italiano" },
|
||||
{ locale: "fa", name: "Persian(فارسی)" },
|
||||
{ locale: "zh", name: "简体中文" },
|
||||
]
|
||||
);
|
||||
|
||||
const App = () => (
|
||||
|
|
5
src/AppContext.jsx
Normal file
5
src/AppContext.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
|
@ -15,6 +15,7 @@ import {
|
|||
useRecordContext,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import { MXCField } from "./media";
|
||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import ReportIcon from "@mui/icons-material/Warning";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
|
@ -89,6 +90,8 @@ export const ReportShow = props => {
|
|||
<TextField source="event_json.type" />
|
||||
<TextField source="event_json.content.msgtype" />
|
||||
<TextField source="event_json.content.body" />
|
||||
<TextField source="event_json.content.info.mimetype" />
|
||||
<MXCField source="event_json.content.url" />
|
||||
<TextField source="event_json.content.format" />
|
||||
<TextField source="event_json.content.formatted_body" />
|
||||
<TextField source="event_json.content.algorithm" />
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
useTranslate,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
useLocales,
|
||||
} from "react-admin";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import {
|
||||
|
@ -21,13 +22,15 @@ import {
|
|||
CircularProgress,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
|
||||
import { useAppContext } from "../AppContext";
|
||||
import {
|
||||
getServerVersion,
|
||||
getSupportedFeatures,
|
||||
getSupportedLoginFlows,
|
||||
getWellKnownUrl,
|
||||
isValidBaseUrl,
|
||||
|
@ -37,7 +40,7 @@ import {
|
|||
const FormBox = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "calc(100vh - 1em)",
|
||||
minHeight: "calc(100vh - 1rem)",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
background: "url(./images/floating-cogs.svg)",
|
||||
|
@ -46,12 +49,12 @@ const FormBox = styled(Box)(({ theme }) => ({
|
|||
backgroundSize: "cover",
|
||||
|
||||
[`& .card`]: {
|
||||
minWidth: "30em",
|
||||
marginTop: "6em",
|
||||
marginBottom: "6em",
|
||||
width: "30rem",
|
||||
marginTop: "6rem",
|
||||
marginBottom: "6rem",
|
||||
},
|
||||
[`& .avatar`]: {
|
||||
margin: "1em",
|
||||
margin: "1rem",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
@ -60,36 +63,49 @@ const FormBox = styled(Box)(({ theme }) => ({
|
|||
},
|
||||
[`& .hint`]: {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.grey[600],
|
||||
},
|
||||
[`& .form`]: {
|
||||
padding: "0 1em 1em 1em",
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .input`]: {
|
||||
marginTop: "1em",
|
||||
[`& .select`]: {
|
||||
marginBottom: "2rem",
|
||||
},
|
||||
[`& .actions`]: {
|
||||
padding: "0 1em 1em 1em",
|
||||
padding: "0 1rem 1rem 1rem",
|
||||
},
|
||||
[`& .serverVersion`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
marginBottom: "1em",
|
||||
marginLeft: "0.5em",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
[`& .matrixVersions`]: {
|
||||
color: theme.palette.grey[500],
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
fontSize: "0.8rem",
|
||||
marginBottom: "1rem",
|
||||
marginLeft: "0.5rem",
|
||||
},
|
||||
}));
|
||||
|
||||
const LoginPage = () => {
|
||||
const login = useLogin();
|
||||
const notify = useNotify();
|
||||
const { restrictBaseUrl } = useAppContext();
|
||||
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
||||
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
|
||||
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||
const [locale, setLocale] = useLocaleState();
|
||||
const locales = useLocales();
|
||||
const translate = useTranslate();
|
||||
const base_url = localStorage.getItem("base_url");
|
||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
||||
const base_url = allowSingleBaseUrl
|
||||
? restrictBaseUrl
|
||||
: localStorage.getItem("base_url");
|
||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||
|
||||
|
@ -127,20 +143,6 @@ const LoginPage = () => {
|
|||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
@ -179,17 +181,27 @@ const LoginPage = () => {
|
|||
const UserData = ({ formData }) => {
|
||||
const form = useFormContext();
|
||||
const [serverVersion, setServerVersion] = useState("");
|
||||
const [matrixVersions, setMatrixVersions] = useState("");
|
||||
|
||||
const handleUsernameChange = _ => {
|
||||
if (formData.base_url || cfg_base_url) return;
|
||||
if (formData.base_url || allowSingleBaseUrl) return;
|
||||
// check if username is a full qualified userId then set base_url accordingly
|
||||
const domain = splitMxid(formData.username)?.domain;
|
||||
if (domain) {
|
||||
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
|
||||
getWellKnownUrl(domain).then(url => {
|
||||
if (
|
||||
allowAnyBaseUrl ||
|
||||
(allowMultipleBaseUrls && restrictBaseUrl.includes(url))
|
||||
)
|
||||
form.setValue("base_url", url);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
||||
form.setValue("base_url", restrictBaseUrl[0]);
|
||||
}
|
||||
if (!isValidBaseUrl(formData.base_url)) return;
|
||||
|
||||
getServerVersion(formData.base_url)
|
||||
|
@ -200,6 +212,14 @@ const LoginPage = () => {
|
|||
)
|
||||
.catch(() => setServerVersion(""));
|
||||
|
||||
getSupportedFeatures(formData.base_url)
|
||||
.then(features =>
|
||||
setMatrixVersions(
|
||||
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
|
||||
)
|
||||
)
|
||||
.catch(() => setMatrixVersions(""));
|
||||
|
||||
// Set SSO Url
|
||||
getSupportedLoginFlows(formData.base_url)
|
||||
.then(loginFlows => {
|
||||
|
@ -211,7 +231,7 @@ const LoginPage = () => {
|
|||
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
||||
})
|
||||
.catch(() => setSSOBaseUrl(""));
|
||||
}, [formData.base_url]);
|
||||
}, [formData.base_url, form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -219,49 +239,56 @@ const LoginPage = () => {
|
|||
<TextInput
|
||||
autoFocus
|
||||
name="username"
|
||||
component={renderInput}
|
||||
label="ra.auth.username"
|
||||
autoComplete="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"
|
||||
autoComplete="current-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
|
||||
select={allowMultipleBaseUrls}
|
||||
autoComplete="url"
|
||||
disabled={loading}
|
||||
readOnly={allowSingleBaseUrl}
|
||||
resettable={allowAnyBaseUrl}
|
||||
fullWidth
|
||||
className="input"
|
||||
validate={[required(), validateBaseUrl]}
|
||||
/>
|
||||
>
|
||||
{allowMultipleBaseUrls &&
|
||||
restrictBaseUrl.map(url => (
|
||||
<MenuItem key={url} value={url}>
|
||||
{url}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextInput>
|
||||
</Box>
|
||||
<Typography className="serverVersion">{serverVersion}</Typography>
|
||||
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
defaultValues={{ base_url: cfg_base_url || base_url }}
|
||||
defaultValues={{ base_url: base_url }}
|
||||
onSubmit={handleSubmit}
|
||||
mode="onTouched"
|
||||
>
|
||||
|
@ -280,19 +307,16 @@ const LoginPage = () => {
|
|||
<Box className="form">
|
||||
<Select
|
||||
value={locale}
|
||||
onChange={e => {
|
||||
setLocale(e.target.value);
|
||||
}}
|
||||
onChange={e => setLocale(e.target.value)}
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
className="input"
|
||||
className="select"
|
||||
>
|
||||
<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>
|
||||
<MenuItem value="fa">Persian(فارسی)</MenuItem>
|
||||
{locales.map(l => (
|
||||
<MenuItem key={l.locale} value={l.locale}>
|
||||
{l.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormDataConsumer>
|
||||
{formDataProps => <UserData {...formDataProps} />}
|
||||
|
|
|
@ -1,14 +1,71 @@
|
|||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { AdminContext } from "react-admin";
|
||||
import LoginPage from "./LoginPage";
|
||||
import { AppContext } from "../AppContext";
|
||||
|
||||
describe("LoginForm", () => {
|
||||
it("renders", () => {
|
||||
it("renders with no restriction to homeserver", () => {
|
||||
render(
|
||||
<AdminContext>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
);
|
||||
|
||||
screen.getByText("synapseadmin.auth.welcome");
|
||||
screen.getByRole("combobox", { name: "" });
|
||||
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||
screen.getByText("ra.auth.password");
|
||||
const baseUrlInput = screen.getByRole("textbox", {
|
||||
name: "synapseadmin.auth.base_url",
|
||||
});
|
||||
expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
|
||||
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||
});
|
||||
|
||||
it("renders with single restricted homeserver", () => {
|
||||
render(
|
||||
<AppContext.Provider
|
||||
value={{ restrictBaseUrl: "https://matrix.example.com" }}
|
||||
>
|
||||
<AdminContext>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
screen.getByText("synapseadmin.auth.welcome");
|
||||
screen.getByRole("combobox", { name: "" });
|
||||
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||
screen.getByText("ra.auth.password");
|
||||
const baseUrlInput = screen.getByRole("textbox", {
|
||||
name: "synapseadmin.auth.base_url",
|
||||
});
|
||||
expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
|
||||
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||
});
|
||||
|
||||
it("renders with multiple restricted homeservers", async () => {
|
||||
render(
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
restrictBaseUrl: [
|
||||
"https://matrix.example.com",
|
||||
"https://matrix.example.org",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<AdminContext>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
screen.getByText("synapseadmin.auth.welcome");
|
||||
screen.getByRole("combobox", { name: "" });
|
||||
screen.getByRole("textbox", { name: "ra.auth.username" });
|
||||
screen.getByText("ra.auth.password");
|
||||
screen.getByRole("combobox", { name: "synapseadmin.auth.base_url" });
|
||||
screen.getByRole("button", { name: "ra.auth.sign_in" });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import get from "lodash/get";
|
||||
import {
|
||||
BooleanInput,
|
||||
Button,
|
||||
|
@ -14,10 +15,12 @@ import {
|
|||
useRefresh,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
||||
import {
|
||||
Box,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
|
@ -27,7 +30,9 @@ import {
|
|||
import IconCancel from "@mui/icons-material/Cancel";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { getMediaUrl } from "../synapse/synapse";
|
||||
|
||||
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
|
@ -333,3 +338,49 @@ export const QuarantineMediaButton = props => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewMediaButton = ({ media_id, label }) => {
|
||||
const translate = useTranslate();
|
||||
const url = getMediaUrl(media_id);
|
||||
return (
|
||||
<Box style={{ whiteSpace: "pre" }}>
|
||||
<Tooltip title={translate("resources.users_media.action.open")}>
|
||||
<span>
|
||||
<Button
|
||||
component={Link}
|
||||
to={url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
|
||||
>
|
||||
<FileOpenIcon />
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const MediaIDField = ({ source }) => {
|
||||
const homeserver = localStorage.getItem("home_server");
|
||||
const record = useRecordContext();
|
||||
if (!record) return null;
|
||||
|
||||
const src = get(record, source)?.toString();
|
||||
if (!src) return null;
|
||||
|
||||
return <ViewMediaButton media_id={`${homeserver}/${src}`} label={src} />;
|
||||
};
|
||||
|
||||
export const MXCField = ({ source }) => {
|
||||
const record = useRecordContext();
|
||||
if (!record) return null;
|
||||
|
||||
const src = get(record, source)?.toString();
|
||||
if (!src) return null;
|
||||
|
||||
const media_id = src.replace("mxc://", "");
|
||||
|
||||
return <ViewMediaButton media_id={media_id} label={src} />;
|
||||
};
|
||||
|
|
|
@ -51,7 +51,11 @@ import { Link } from "react-router-dom";
|
|||
import AvatarField from "./AvatarField";
|
||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||
import { DeviceRemoveButton } from "./devices";
|
||||
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
|
||||
import {
|
||||
MediaIDField,
|
||||
ProtectMediaButton,
|
||||
QuarantineMediaButton,
|
||||
} from "./media";
|
||||
|
||||
const choices_medium = [
|
||||
{ id: "email", name: "resources.users.email" },
|
||||
|
@ -449,13 +453,13 @@ export const UserEdit = props => {
|
|||
sort={{ field: "created_ts", order: "DESC" }}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<MediaIDField source="media_id" />
|
||||
<DateField source="created_ts" showTime options={date_format} />
|
||||
<DateField
|
||||
source="last_access_ts"
|
||||
showTime
|
||||
options={date_format}
|
||||
/>
|
||||
<TextField source="media_id" />
|
||||
<NumberField source="media_length" />
|
||||
<TextField source="media_type" />
|
||||
<TextField source="upload_name" />
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import germanMessages from "ra-language-german";
|
||||
import { formalGermanMessages } from "@haleos/ra-language-german";
|
||||
|
||||
const de = {
|
||||
...germanMessages,
|
||||
...formalGermanMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Heimserver URL",
|
||||
welcome: "Willkommen bei Synapse-admin",
|
||||
server_version: "Synapse Version",
|
||||
supports_specs: "unterstützt Matrix-Specs",
|
||||
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
|
||||
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
||||
url_error: "Keine gültige Matrix Server URL",
|
||||
|
@ -207,6 +208,9 @@ const de = {
|
|||
format: "Nachrichtenformat",
|
||||
formatted_body: "Formatierter Nachrichteninhalt",
|
||||
algorithm: "Verschlüsselungsalgorithmus",
|
||||
info: {
|
||||
mimetype: "Typ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -255,6 +259,9 @@ const de = {
|
|||
created_ts: "Erstellt",
|
||||
last_access_ts: "Letzter Zugriff",
|
||||
},
|
||||
action: {
|
||||
open: "Mediendatei in neuem Fenster öffnen",
|
||||
},
|
||||
},
|
||||
delete_media: {
|
||||
name: "Medien",
|
||||
|
@ -388,37 +395,5 @@ const de = {
|
|||
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
...germanMessages.ra,
|
||||
action: {
|
||||
...germanMessages.ra.action,
|
||||
unselect: "Abwählen",
|
||||
},
|
||||
auth: {
|
||||
...germanMessages.ra.auth,
|
||||
auth_check_error: "Anmeldung fehlgeschlagen",
|
||||
},
|
||||
input: {
|
||||
...germanMessages.ra.input,
|
||||
password: {
|
||||
...germanMessages.ra.input.password,
|
||||
toggle_hidden: "Anzeigen",
|
||||
toggle_visible: "Verstecken",
|
||||
},
|
||||
},
|
||||
notification: {
|
||||
...germanMessages.ra.notification,
|
||||
logged_out: "Abgemeldet",
|
||||
},
|
||||
page: {
|
||||
...germanMessages.ra.page,
|
||||
empty: "Keine Einträge vorhanden",
|
||||
invite: "",
|
||||
},
|
||||
navigation: {
|
||||
...germanMessages.ra.navigation,
|
||||
skip_nav: "Zum Inhalt springen",
|
||||
},
|
||||
},
|
||||
};
|
||||
export default de;
|
||||
|
|
|
@ -7,6 +7,7 @@ const en = {
|
|||
base_url: "Homeserver URL",
|
||||
welcome: "Welcome to Synapse-admin",
|
||||
server_version: "Synapse version",
|
||||
supports_specs: "supports Matrix specs",
|
||||
username_error: "Please enter fully qualified user ID: '@user:domain'",
|
||||
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||
url_error: "Not a valid Matrix server URL",
|
||||
|
@ -204,6 +205,10 @@ const en = {
|
|||
format: "format",
|
||||
formatted_body: "formatted content",
|
||||
algorithm: "algorithm",
|
||||
url: "URL",
|
||||
info: {
|
||||
mimetype: "Type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -252,6 +257,9 @@ const en = {
|
|||
created_ts: "Created",
|
||||
last_access_ts: "Last access",
|
||||
},
|
||||
action: {
|
||||
open: "Open media file in new window",
|
||||
},
|
||||
},
|
||||
delete_media: {
|
||||
name: "Media",
|
||||
|
@ -371,19 +379,19 @@ const en = {
|
|||
},
|
||||
action: { reconnect: "Reconnect" },
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Registration tokens",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Valid token",
|
||||
uses_allowed: "Uses allowed",
|
||||
pending: "Pending",
|
||||
completed: "Completed",
|
||||
expiry_time: "Expiry time",
|
||||
length: "Length",
|
||||
registration_tokens: {
|
||||
name: "Registration tokens",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Valid token",
|
||||
uses_allowed: "Uses allowed",
|
||||
pending: "Pending",
|
||||
completed: "Completed",
|
||||
expiry_time: "Expiry time",
|
||||
length: "Length",
|
||||
},
|
||||
helper: { length: "Length of the token if no token is given." },
|
||||
},
|
||||
helper: { length: "Length of the token if no token is given." },
|
||||
},
|
||||
};
|
||||
export default en;
|
||||
|
|
|
@ -364,19 +364,19 @@ const fa = {
|
|||
},
|
||||
action: { reconnect: "دوباره وصل شوید" },
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "توکن های ثبت نام",
|
||||
fields: {
|
||||
token: "توکن",
|
||||
valid: "توکن معتبر",
|
||||
uses_allowed: "موارد استفاده مجاز",
|
||||
pending: "انتظار",
|
||||
completed: "تکمیل شد",
|
||||
expiry_time: "زمان انقضا",
|
||||
length: "طول",
|
||||
registration_tokens: {
|
||||
name: "توکن های ثبت نام",
|
||||
fields: {
|
||||
token: "توکن",
|
||||
valid: "توکن معتبر",
|
||||
uses_allowed: "موارد استفاده مجاز",
|
||||
pending: "انتظار",
|
||||
completed: "تکمیل شد",
|
||||
expiry_time: "زمان انقضا",
|
||||
length: "طول",
|
||||
},
|
||||
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
|
||||
},
|
||||
helper: { length: "طول توکن در صورت عدم ارائه توکن." },
|
||||
},
|
||||
};
|
||||
export default fa;
|
||||
|
|
|
@ -356,21 +356,21 @@ const fr = {
|
|||
send_failure: "Une erreur s'est produite",
|
||||
},
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Jetons d'inscription",
|
||||
fields: {
|
||||
token: "Jeton",
|
||||
valid: "Jeton valide",
|
||||
uses_allowed: "Nombre d'inscription autorisées",
|
||||
pending: "Nombre d'inscription en cours",
|
||||
completed: "Nombre d'inscription accomplie",
|
||||
expiry_time: "Date d'expiration",
|
||||
length: "Longueur",
|
||||
},
|
||||
helper: {
|
||||
length:
|
||||
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
||||
registration_tokens: {
|
||||
name: "Jetons d'inscription",
|
||||
fields: {
|
||||
token: "Jeton",
|
||||
valid: "Jeton valide",
|
||||
uses_allowed: "Nombre d'inscription autorisées",
|
||||
pending: "Nombre d'inscription en cours",
|
||||
completed: "Nombre d'inscription accomplie",
|
||||
expiry_time: "Date d'expiration",
|
||||
length: "Longueur",
|
||||
},
|
||||
helper: {
|
||||
length:
|
||||
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -367,19 +367,19 @@ const it = {
|
|||
},
|
||||
action: { reconnect: "Riconnetti" },
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Token di registrazione",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Token valido",
|
||||
uses_allowed: "Usi permessi",
|
||||
pending: "In attesa",
|
||||
completed: "Completato",
|
||||
expiry_time: "Data della scadenza",
|
||||
length: "Lunghezza",
|
||||
registration_tokens: {
|
||||
name: "Token di registrazione",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Token valido",
|
||||
uses_allowed: "Usi permessi",
|
||||
pending: "In attesa",
|
||||
completed: "Completato",
|
||||
expiry_time: "Data della scadenza",
|
||||
length: "Lunghezza",
|
||||
},
|
||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||
},
|
||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||
},
|
||||
};
|
||||
export default it;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import chineseMessages from "ra-language-chinese";
|
||||
import chineseMessages from "@haxqer/ra-language-chinese";
|
||||
|
||||
const zh = {
|
||||
...chineseMessages,
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
import App from "./App";
|
||||
import { AppContext } from "./AppContext";
|
||||
|
||||
fetch("config.json")
|
||||
.then(res => res.json())
|
||||
.then(props =>
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<AppContext.Provider value={props}>
|
||||
<App />
|
||||
</AppContext.Provider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -2,10 +2,7 @@ import { fetchUtils } from "react-admin";
|
|||
|
||||
const authProvider = {
|
||||
// called when the user attempts to log in
|
||||
login: ({ base_url, username, password, loginToken }) => {
|
||||
// force homeserver for protection in case the form is manipulated
|
||||
base_url = process.env.REACT_APP_SERVER || base_url;
|
||||
|
||||
login: async ({ base_url, username, password, loginToken }) => {
|
||||
console.log("login ");
|
||||
const options = {
|
||||
method: "POST",
|
||||
|
@ -38,15 +35,14 @@ const authProvider = {
|
|||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||
|
||||
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
||||
localStorage.setItem("home_server", json.home_server);
|
||||
localStorage.setItem("user_id", json.user_id);
|
||||
localStorage.setItem("access_token", json.access_token);
|
||||
localStorage.setItem("device_id", json.device_id);
|
||||
});
|
||||
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
||||
localStorage.setItem("home_server", json.home_server);
|
||||
localStorage.setItem("user_id", json.user_id);
|
||||
localStorage.setItem("access_token", json.access_token);
|
||||
localStorage.setItem("device_id", json.device_id);
|
||||
},
|
||||
// called when the user clicks on the logout button
|
||||
logout: () => {
|
||||
logout: async () => {
|
||||
console.log("logout");
|
||||
|
||||
const logout_api_url =
|
||||
|
@ -62,11 +58,9 @@ const authProvider = {
|
|||
};
|
||||
|
||||
if (typeof access_token === "string") {
|
||||
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
||||
localStorage.removeItem("access_token");
|
||||
});
|
||||
await fetchUtils.fetchJson(logout_api_url, options);
|
||||
localStorage.removeItem("access_token");
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
// called when the API returns an error
|
||||
checkError: ({ status }) => {
|
||||
|
|
135
src/synapse/authProvider.test.js
Normal file
135
src/synapse/authProvider.test.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
import authProvider from "./authProvider";
|
||||
|
||||
describe("authProvider", () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("login", () => {
|
||||
it("should successfully login with username and password", async () => {
|
||||
fetch.once(
|
||||
JSON.stringify({
|
||||
home_server: "example.com",
|
||||
user_id: "@user:example.com",
|
||||
access_token: "foobar",
|
||||
device_id: "some_device",
|
||||
})
|
||||
);
|
||||
|
||||
const ret = await authProvider.login({
|
||||
base_url: "http://example.com",
|
||||
username: "@user:example.com",
|
||||
password: "secret",
|
||||
});
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(fetch).toBeCalledWith(
|
||||
"http://example.com/_matrix/client/r0/login",
|
||||
{
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
"Content-Type": ["application/json"],
|
||||
}),
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully login with token", async () => {
|
||||
fetch.once(
|
||||
JSON.stringify({
|
||||
home_server: "example.com",
|
||||
user_id: "@user:example.com",
|
||||
access_token: "foobar",
|
||||
device_id: "some_device",
|
||||
})
|
||||
);
|
||||
|
||||
const ret = await authProvider.login({
|
||||
base_url: "https://example.com/",
|
||||
loginToken: "login_token",
|
||||
});
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(fetch).toBeCalledWith(
|
||||
"https://example.com/_matrix/client/r0/login",
|
||||
{
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
"Content-Type": ["application/json"],
|
||||
}),
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
it("should remove the access_token from localStorage", async () => {
|
||||
localStorage.setItem("base_url", "example.com");
|
||||
localStorage.setItem("access_token", "foo");
|
||||
fetch.mockResponse(JSON.stringify({}));
|
||||
|
||||
await authProvider.logout();
|
||||
|
||||
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
||||
headers: new Headers({
|
||||
Accept: ["application/json"],
|
||||
Authorization: ["Bearer foo"],
|
||||
}),
|
||||
method: "POST",
|
||||
user: { authenticated: true, token: "Bearer foo" },
|
||||
});
|
||||
expect(localStorage.getItem("access_token")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkError", () => {
|
||||
it("should resolve if error.status is not 401 or 403", async () => {
|
||||
await expect(
|
||||
authProvider.checkError({ status: 200 })
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should reject if error.status is 401", async () => {
|
||||
await expect(
|
||||
authProvider.checkError({ status: 401 })
|
||||
).rejects.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should reject if error.status is 403", async () => {
|
||||
await expect(
|
||||
authProvider.checkError({ status: 403 })
|
||||
).rejects.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkAuth", () => {
|
||||
it("should reject when not logged in", async () => {
|
||||
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should resolve when logged in", async () => {
|
||||
localStorage.setItem("access_token", "foobar");
|
||||
|
||||
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPermissions", () => {
|
||||
it("should do nothing", async () => {
|
||||
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -348,7 +348,7 @@ function getSearchOrder(order) {
|
|||
}
|
||||
|
||||
const dataProvider = {
|
||||
getList: (resource, params) => {
|
||||
getList: async (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const {
|
||||
user_id,
|
||||
|
@ -383,13 +383,14 @@ const dataProvider = {
|
|||
const endpoint_url = homeserver + res.path;
|
||||
const url = `${endpoint_url}?${stringify(query)}`;
|
||||
|
||||
return jsonClient(url).then(({ json }) => ({
|
||||
const { json } = await jsonClient(url);
|
||||
return {
|
||||
data: json[res.data].map(res.map),
|
||||
total: res.total(json, from, perPage),
|
||||
}));
|
||||
};
|
||||
},
|
||||
|
||||
getOne: (resource, params) => {
|
||||
getOne: async (resource, params) => {
|
||||
console.log("getOne " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -397,14 +398,13 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
|
||||
({ json }) => ({
|
||||
data: res.map(json),
|
||||
})
|
||||
const { json } = await jsonClient(
|
||||
`${endpoint_url}/${encodeURIComponent(params.id)}`
|
||||
);
|
||||
return { data: res.map(json) };
|
||||
},
|
||||
|
||||
getMany: (resource, params) => {
|
||||
getMany: async (resource, params) => {
|
||||
console.log("getMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -412,17 +412,18 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return Promise.all(
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(id =>
|
||||
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
|
||||
)
|
||||
).then(responses => ({
|
||||
);
|
||||
return {
|
||||
data: responses.map(({ json }) => res.map(json)),
|
||||
total: responses.length,
|
||||
}));
|
||||
};
|
||||
},
|
||||
|
||||
getManyReference: (resource, params) => {
|
||||
getManyReference: async (resource, params) => {
|
||||
console.log("getManyReference " + resource);
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
|
@ -442,13 +443,14 @@ const dataProvider = {
|
|||
const ref = res["reference"](params.id);
|
||||
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
||||
|
||||
return jsonClient(endpoint_url).then(({ headers, json }) => ({
|
||||
const { json } = await jsonClient(endpoint_url);
|
||||
return {
|
||||
data: json[res.data].map(res.map),
|
||||
total: res.total(json, from, perPage),
|
||||
}));
|
||||
};
|
||||
},
|
||||
|
||||
update: (resource, params) => {
|
||||
update: async (resource, params) => {
|
||||
console.log("update " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -456,15 +458,17 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params.data, filterNullValues),
|
||||
}).then(({ json }) => ({
|
||||
data: res.map(json),
|
||||
}));
|
||||
const { json } = await jsonClient(
|
||||
`${endpoint_url}/${encodeURIComponent(params.id)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params.data, filterNullValues),
|
||||
}
|
||||
);
|
||||
return { data: res.map(json) };
|
||||
},
|
||||
|
||||
updateMany: (resource, params) => {
|
||||
updateMany: async (resource, params) => {
|
||||
console.log("updateMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -472,7 +476,7 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return Promise.all(
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(
|
||||
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
|
||||
{
|
||||
|
@ -480,12 +484,11 @@ const dataProvider = {
|
|||
body: JSON.stringify(params.data, filterNullValues),
|
||||
}
|
||||
)
|
||||
).then(responses => ({
|
||||
data: responses.map(({ json }) => json),
|
||||
}));
|
||||
);
|
||||
return { data: responses.map(({ json }) => json) };
|
||||
},
|
||||
|
||||
create: (resource, params) => {
|
||||
create: async (resource, params) => {
|
||||
console.log("create " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -495,15 +498,14 @@ const dataProvider = {
|
|||
|
||||
const create = res["create"](params.data);
|
||||
const endpoint_url = homeserver + create.endpoint;
|
||||
return jsonClient(endpoint_url, {
|
||||
const { json } = await jsonClient(endpoint_url, {
|
||||
method: create.method,
|
||||
body: JSON.stringify(create.body, filterNullValues),
|
||||
}).then(({ json }) => ({
|
||||
data: res.map(json),
|
||||
}));
|
||||
});
|
||||
return { data: res.map(json) };
|
||||
},
|
||||
|
||||
createMany: (resource, params) => {
|
||||
createMany: async (resource, params) => {
|
||||
console.log("createMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -511,7 +513,7 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
if (!("create" in res)) return Promise.reject();
|
||||
|
||||
return Promise.all(
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(id => {
|
||||
params.data.id = id;
|
||||
const cre = res["create"](params.data);
|
||||
|
@ -521,12 +523,11 @@ const dataProvider = {
|
|||
body: JSON.stringify(cre.body, filterNullValues),
|
||||
});
|
||||
})
|
||||
).then(responses => ({
|
||||
data: responses.map(({ json }) => json),
|
||||
}));
|
||||
);
|
||||
return { data: responses.map(({ json }) => json) };
|
||||
},
|
||||
|
||||
delete: (resource, params) => {
|
||||
delete: async (resource, params) => {
|
||||
console.log("delete " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -536,24 +537,22 @@ const dataProvider = {
|
|||
if ("delete" in res) {
|
||||
const del = res["delete"](params);
|
||||
const endpoint_url = homeserver + del.endpoint;
|
||||
return jsonClient(endpoint_url, {
|
||||
const { json } = await jsonClient(endpoint_url, {
|
||||
method: "method" in del ? del.method : "DELETE",
|
||||
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||
}).then(({ json }) => ({
|
||||
data: json,
|
||||
}));
|
||||
});
|
||||
return { data: json };
|
||||
} else {
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return jsonClient(`${endpoint_url}/${params.id}`, {
|
||||
const { json } = await jsonClient(`${endpoint_url}/${params.id}`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params.previousData, filterNullValues),
|
||||
}).then(({ json }) => ({
|
||||
data: json,
|
||||
}));
|
||||
});
|
||||
return { data: json };
|
||||
}
|
||||
},
|
||||
|
||||
deleteMany: (resource, params) => {
|
||||
deleteMany: async (resource, params) => {
|
||||
console.log("deleteMany " + resource);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
@ -561,7 +560,7 @@ const dataProvider = {
|
|||
const res = resourceMap[resource];
|
||||
|
||||
if ("delete" in res) {
|
||||
return Promise.all(
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(id => {
|
||||
const del = res["delete"]({ ...params, id: id });
|
||||
const endpoint_url = homeserver + del.endpoint;
|
||||
|
@ -570,21 +569,21 @@ const dataProvider = {
|
|||
body: "body" in del ? JSON.stringify(del.body) : null,
|
||||
});
|
||||
})
|
||||
).then(responses => ({
|
||||
);
|
||||
return {
|
||||
data: responses.map(({ json }) => json),
|
||||
}));
|
||||
};
|
||||
} else {
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return Promise.all(
|
||||
const responses = await Promise.all(
|
||||
params.ids.map(id =>
|
||||
jsonClient(`${endpoint_url}/${id}`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params.data, filterNullValues),
|
||||
})
|
||||
)
|
||||
).then(responses => ({
|
||||
data: responses.map(({ json }) => json),
|
||||
}));
|
||||
);
|
||||
return { data: responses.map(({ json }) => json) };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -36,6 +36,13 @@ export const getServerVersion = async baseUrl => {
|
|||
return response.json.server_version;
|
||||
};
|
||||
|
||||
/** Get supported Matrix features */
|
||||
export const getSupportedFeatures = async baseUrl => {
|
||||
const versionUrl = `${baseUrl}/_matrix/client/versions`;
|
||||
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
|
||||
return response.json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get supported login flows
|
||||
* @param baseUrl the base URL of the homeserver
|
||||
|
@ -46,3 +53,8 @@ export const getSupportedLoginFlows = async baseUrl => {
|
|||
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
|
||||
return response.json.flows;
|
||||
};
|
||||
|
||||
export const getMediaUrl = media_id => {
|
||||
const baseUrl = localStorage.getItem("base_url");
|
||||
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
||||
};
|
||||
|
|
15
vite.config.js
Normal file
15
vite.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { vitePluginVersionMark } from "vite-plugin-version-mark";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
vitePluginVersionMark({
|
||||
command: "git describe --tags",
|
||||
ifMeta: true,
|
||||
ifLog: true,
|
||||
ifGlobal: true,
|
||||
}),
|
||||
],
|
||||
});
|
Loading…
Reference in a new issue