mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Merge staging 12feb (#7993)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
+90
-51
@@ -40,6 +40,8 @@ env:
|
||||
rush.json
|
||||
.prettierrc
|
||||
tools
|
||||
workers
|
||||
ws-tests
|
||||
PublishTempFolder: publish_artifacts
|
||||
INIT_SCRIPTS_BRANCH: 'unified-init-scripts'
|
||||
|
||||
@@ -81,13 +83,13 @@ jobs:
|
||||
run: node common/scripts/install-run-rush.js model-version
|
||||
|
||||
- name: Building...
|
||||
run: node common/scripts/install-run-rush.js build -p 20
|
||||
run: node common/scripts/install-run-rush.js build
|
||||
|
||||
- name: Bundle...
|
||||
run: node common/scripts/install-run-rush.js bundle -p 20
|
||||
run: node common/scripts/install-run-rush.js bundle
|
||||
|
||||
- name: Validate...
|
||||
run: node common/scripts/install-run-rush.js validate -p 20
|
||||
run: node common/scripts/install-run-rush.js validate
|
||||
|
||||
- name: Cache build results
|
||||
uses: actions/cache@v4
|
||||
@@ -154,7 +156,7 @@ jobs:
|
||||
[ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ]
|
||||
echo '================================================================'
|
||||
- name: Formatting...
|
||||
run: node common/scripts/install-run-rush.js fast-format
|
||||
run: node common/scripts/install-run-rush.js fast-format --branch ${{ github.base_ref }}
|
||||
- name: Check files formatting
|
||||
run: |
|
||||
echo '================================================================'
|
||||
@@ -223,7 +225,7 @@ jobs:
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
|
||||
- name: Docker Build
|
||||
run: node common/scripts/install-run-rush.js docker:build -p 20
|
||||
run: node common/scripts/install-run-rush.js docker
|
||||
env:
|
||||
DOCKER_CLI_HINTS: false
|
||||
- name: Prepare server
|
||||
@@ -260,10 +262,8 @@ jobs:
|
||||
run: |
|
||||
cd ./tests/sanity
|
||||
mkdir logs
|
||||
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
|
||||
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
|
||||
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
|
||||
docker logs $(docker ps | grep collaborator | cut -f 1 -d ' ') > logs/collaborator.log
|
||||
cd logs
|
||||
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
|
||||
- name: Docker Build
|
||||
run: node common/scripts/install-run-rush.js docker:build -p 20
|
||||
run: node common/scripts/install-run-rush.js docker
|
||||
env:
|
||||
DOCKER_CLI_HINTS: false
|
||||
- name: Prepare server
|
||||
@@ -361,9 +361,8 @@ jobs:
|
||||
run: |
|
||||
cd ./tests/sanity
|
||||
mkdir logs
|
||||
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
|
||||
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
|
||||
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
|
||||
cd logs
|
||||
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -405,12 +404,12 @@ jobs:
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
|
||||
- name: Docker Build
|
||||
run: node common/scripts/install-run-rush.js docker:build -p 20
|
||||
run: node common/scripts/install-run-rush.js docker
|
||||
env:
|
||||
DOCKER_CLI_HINTS: false
|
||||
- name: Configure /etc/hosts
|
||||
run: |
|
||||
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
|
||||
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
|
||||
- name: Prepare server
|
||||
run: |
|
||||
cd ./qms-tests
|
||||
@@ -429,50 +428,90 @@ jobs:
|
||||
run: |
|
||||
cd ./qms-tests/sanity
|
||||
mkdir logs
|
||||
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
|
||||
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
|
||||
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
|
||||
cd logs
|
||||
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-results-qms
|
||||
path: ./qms-tests/sanity/playwright-report/
|
||||
# - name: Get Allure history
|
||||
# uses: actions/checkout@v4
|
||||
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# ref: gh-pages
|
||||
# path: gh-pages
|
||||
# - name: Generates Allure Report
|
||||
# uses: simple-elf/allure-report-action@master
|
||||
# if: always()
|
||||
# id: allure-report
|
||||
# with:
|
||||
# allure_results: ./qms-tests/sanity/allure-results/
|
||||
# gh_pages: gh-pages
|
||||
# allure_report: allure-report
|
||||
# allure_history: allure-history
|
||||
# - name: Upload allure test results
|
||||
# if: always()
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: allure-report-qms
|
||||
# path: ./allure-report/
|
||||
# - name: Deploy report to Github Pages
|
||||
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
# uses: peaceiris/actions-gh-pages@v4
|
||||
# with:
|
||||
# PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# PUBLISH_BRANCH: gh-pages
|
||||
# PUBLISH_DIR: allure-history
|
||||
- name: Upload Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-logs-qms
|
||||
path: ./qms-tests/sanity/logs
|
||||
uitest-workspaces:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-platform
|
||||
with:
|
||||
path: |
|
||||
common/temp
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Checking for mis-matching dependencies...
|
||||
run: node common/scripts/install-run-rush.js check
|
||||
|
||||
- name: Installing...
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
|
||||
- name: Build sanity-tests suite
|
||||
run: node common/scripts/install-run-rush.js build -t @hcengineering/tests-sanity
|
||||
|
||||
- name: Docker Build
|
||||
run: node common/scripts/install-run-rush.js docker
|
||||
env:
|
||||
DOCKER_CLI_HINTS: false
|
||||
- name: Configure /etc/hosts
|
||||
run: |
|
||||
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
|
||||
- name: Prepare server
|
||||
run: |
|
||||
cd ./ws-tests
|
||||
export DO_CLEAN=true
|
||||
./prepare.sh
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd ./ws-tests/sanity
|
||||
node ../../common/scripts/install-run-rushx.js ci
|
||||
- name: Run UI tests
|
||||
run: |
|
||||
cd ./ws-tests/sanity
|
||||
node ../../common/scripts/install-run-rushx.js uitest
|
||||
- name: 'Store docker logs'
|
||||
if: always()
|
||||
run: |
|
||||
cd ./ws-tests/sanity
|
||||
mkdir logs
|
||||
cd logs
|
||||
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-results-ws
|
||||
path: ./ws-tests/sanity/playwright-report/
|
||||
- name: Upload Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-logs-ws
|
||||
path: ./ws-tests/sanity/logs
|
||||
docker-build:
|
||||
needs: [build, test, svelte-check, uitest]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -531,9 +570,9 @@ jobs:
|
||||
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
|
||||
- name: Docker build love-agent
|
||||
run: |
|
||||
cd ./services/ai-bot/love-agent
|
||||
pnpm install && pnpm build
|
||||
pnpm docker:build -v
|
||||
cd ./services/ai-bot/love-agent
|
||||
pnpm install && pnpm build
|
||||
pnpm docker:build -v
|
||||
env:
|
||||
DOCKER_CLI_HINTS: false
|
||||
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
|
||||
@@ -590,7 +629,7 @@ jobs:
|
||||
- name: Model version from git tags
|
||||
run: node common/scripts/install-run-rush.js model-version
|
||||
- name: Package
|
||||
run: node common/scripts/install-run-rush.js package --to desktop -p 20 -v
|
||||
run: node common/scripts/install-run-rush.js package --to desktop -v
|
||||
- name: Package JSON
|
||||
run: |
|
||||
cd desktop-package
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ temp/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
tests/sanity/screenshots
|
||||
*tests/sanity/screenshots
|
||||
|
||||
# Runtime data
|
||||
*.pid
|
||||
|
||||
Vendored
+65
@@ -70,6 +70,38 @@
|
||||
"cwd": "${workspaceRoot}/pods/server",
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug server(Test)",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["src/__start.ts"],
|
||||
"env": {
|
||||
"FULLTEXT_URL": "http://localhost:4710",
|
||||
// "DB_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "postgresql://postgres:example@localhost:5432",
|
||||
"DB_URL": "postgresql://root@host.docker.internal:26258/defaultdb?sslmode=disable",
|
||||
// "GREEN_URL": "http://host.docker.internal:6767?token=secret",
|
||||
"SERVER_PORT": "3335",
|
||||
"METRICS_CONSOLE": "false",
|
||||
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
|
||||
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
|
||||
"SERVER_SECRET": "secret",
|
||||
"ENABLE_CONSOLE": "true",
|
||||
"COLLABORATOR_URL": "ws://localhost:3079",
|
||||
"FRONT_URL": "http://localhost:8083",
|
||||
"ACCOUNTS_URL": "http://localhost:3003",
|
||||
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
|
||||
"MODEL_VERSION": "0.6.435",
|
||||
"STATS_URL": "http://host.docker.internal:4901"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
"showAsyncStacks": true,
|
||||
"outputCapture": "std",
|
||||
"sourceMaps": true,
|
||||
"cwd": "${workspaceRoot}/pods/server",
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug Fulltext",
|
||||
"type": "node",
|
||||
@@ -132,6 +164,39 @@
|
||||
"cwd": "${workspaceRoot}/pods/account",
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug Account(Staging)",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["src/__start.ts"],
|
||||
"env": {
|
||||
"MONGO_URL": "mongodb://localhost:27018",
|
||||
"DB_URL": "mongodb://localhost:27018",
|
||||
// "DB_URL": "postgresql://postgres:example@localhost:5432",
|
||||
"SERVER_SECRET": "secret",
|
||||
"REGION_INFO":"|Mongo;pg|Postgres;cockroach|CockroachDB",
|
||||
"TRANSACTOR_URL": "ws://host.docker.internal:3334;;,ws://host.docker.internal:3335;;europe",
|
||||
"ACCOUNTS_URL": "http://localhost:3003",
|
||||
"ACCOUNT_PORT": "3003",
|
||||
"FRONT_URL": "http://localhost:8083",
|
||||
"STATS_URL": "http://host.docker.internal:4901",
|
||||
"SES_URL": "",
|
||||
// "DB_NS": "account-2",
|
||||
// "WS_LIVENESS_DAYS": "1",
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"MINIO_ENDPOINT": "localhost"
|
||||
// "DISABLE_SIGNUP": "true",
|
||||
// "INIT_SCRIPT_URL": "https://raw.githubusercontent.com/hcengineering/init/main/script.yaml",
|
||||
// "INIT_WORKSPACE": "onboarding",
|
||||
},
|
||||
"runtimeVersion": "20",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
"outputCapture": "std",
|
||||
"cwd": "${workspaceRoot}/pods/account",
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug Stats",
|
||||
"type": "node",
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
"summary": "Format changed projects",
|
||||
"description": "Format and autofix linting issues in changed projects",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "./common/scripts/each-diff.sh rushx format --force"
|
||||
"shellCommand": "./common/scripts/fast-format.sh"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
@@ -367,6 +367,16 @@
|
||||
"shortName": "-f",
|
||||
"description": "Force formatting",
|
||||
"associatedCommands": ["format"]
|
||||
},
|
||||
{
|
||||
"parameterKind": "string",
|
||||
"argumentName": "BRANCH",
|
||||
"required": false,
|
||||
"associatedPhases": [],
|
||||
"shortName": "-b",
|
||||
"longName": "--branch",
|
||||
"description": "Force formatting of branch",
|
||||
"associatedCommands": ["fast-format"]
|
||||
}
|
||||
// {
|
||||
// /**
|
||||
|
||||
Generated
+29
@@ -1087,6 +1087,9 @@ importers:
|
||||
'@rush-temp/tests-sanity':
|
||||
specifier: file:./projects/tests-sanity.tgz
|
||||
version: file:projects/tests-sanity.tgz
|
||||
'@rush-temp/tests-ws-sanity':
|
||||
specifier: file:./projects/tests-ws-sanity.tgz
|
||||
version: file:projects/tests-ws-sanity.tgz
|
||||
'@rush-temp/text':
|
||||
specifier: file:./projects/text.tgz
|
||||
version: file:projects/text.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
|
||||
@@ -5166,6 +5169,10 @@ packages:
|
||||
resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
|
||||
resolution: {integrity: sha512-2y+gKxse3ozzKeoi1TF7Qfv7Y1fWy9Z6HiqSsEeQgGPpv396cH7ZfpSJ/qwsxO66c6iq8LUuBtzZ4AlquHfG3w==, tarball: file:projects/tests-ws-sanity.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/text-core@file:projects/text-core.tgz':
|
||||
resolution: {integrity: sha512-sQ1C/LTDJ6RoC7YHYOCrRGsOHerkeumxp9lRjRaTmjc65mAFtiizz4eYFlkbSVSQvahSdD5KMT1G+wvtmLq7Gg==, tarball: file:projects/text-core.tgz}
|
||||
version: 0.0.0
|
||||
@@ -24910,6 +24917,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
|
||||
dependencies:
|
||||
'@faker-js/faker': 8.4.1
|
||||
'@playwright/test': 1.49.1
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
|
||||
allure-js-commons: 3.0.7(allure-playwright@3.0.7(@playwright/test@1.49.1))
|
||||
allure-playwright: 3.0.7(@playwright/test@1.49.1)
|
||||
cross-env: 7.0.3
|
||||
dotenv: 16.0.3
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
prettier: 3.2.5
|
||||
typescript: 5.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rush-temp/text-core@file:projects/text-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)':
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
FILES=$(git diff origin/develop --name-only --diff-filter=ACMR | sed 's| |\\ |g')
|
||||
branch=${BASE_BRANCH:-develop}
|
||||
|
||||
FILES=$(git diff origin/$branch --name-only --diff-filter=ACMR | sed 's| |\\ |g')
|
||||
[ -z "$FILES" ] && exit 0
|
||||
|
||||
roots=$(rush list -p --json | grep "path" | cut -f 2 -d ':' | cut -f 2 -d '"')
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
echo $@
|
||||
BASE_BRANCH=${1:-develop}
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--branch) BASE_BRANCH="$2"; shift ;;
|
||||
*) ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
export BASE_BRANCH
|
||||
./common/scripts/each-diff.sh rushx format --force
|
||||
@@ -41,8 +41,6 @@ async function doBackup (dirName: string, token: string, endpoint: string, wsIds
|
||||
ctx.info('do backup', { ...wsIds, endpoint })
|
||||
await backup(ctx, endpoint, wsIds, storage, {
|
||||
force: true,
|
||||
freshBackup: false,
|
||||
clean: false,
|
||||
skipDomains: [],
|
||||
timeout: 0,
|
||||
connectTimeout: 60 * 1000,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"analyze": "rm -rf ./dist && cross-env NODE_ENV=production webpack --profile --json > stats.json",
|
||||
"show": "webpack-bundle-analyzer stats.json dist",
|
||||
"dev-server": "cross-env USE_CACHE=false CLIENT_TYPE=dev-server webpack serve",
|
||||
"dev-server-test": "cross-env USE_CACHE=false CLIENT_TYPE=dev-server-test webpack serve",
|
||||
"dev-worker": "cross-env USE_CACHE=false CLIENT_TYPE=dev-worker webpack serve",
|
||||
"dev-worker-local": "cross-env USE_CACHE=false CLIENT_TYPE=dev-worker-local webpack serve",
|
||||
"dev-server-cache": "cross-env USE_CACHE=true CLIENT_TYPE=dev-server webpack serve",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ACCOUNTS_URL": "/account",
|
||||
"COLLABORATOR_URL": "ws://localhost:3079",
|
||||
"UPLOAD_URL": "/files",
|
||||
"TELEGRAM_URL": "http://localhost:8088",
|
||||
"GMAIL_URL": "http://localhost:8089",
|
||||
"CALENDAR_URL": "http://localhost:8096",
|
||||
"REKONI_URL": "/rekoni",
|
||||
"GITHUB_APP": "uberflow-dev",
|
||||
"GITHUB_CLIENTID": "Iv1.43f9cac43bd68617",
|
||||
"GITHUB_URL": "http://localhost:3501",
|
||||
"LAST_NAME_FIRST": "true",
|
||||
"PRINT_URL": "http://localhost:4006",
|
||||
"SIGN_URL": "http://localhost:4006",
|
||||
"ANALYTICS_COLLECTOR_URL": "http://localhost:4018",
|
||||
"AI_URL": "http://localhost:4011",
|
||||
"BRANDING_URL": "/branding.json",
|
||||
"VERSION": null,
|
||||
"MODEL_VERSION": null,
|
||||
"STATS_URL": "http://localhost:4901",
|
||||
"PASSWORD_STRICTNESS": "none"
|
||||
}
|
||||
@@ -196,6 +196,7 @@ const configs: Record<string, string> = {
|
||||
'dev-huly': '/config-huly.json',
|
||||
'dev-bold': '/config.json',
|
||||
'dev-server': '/config.json',
|
||||
'dev-server-test': '/config-test.json',
|
||||
'dev-worker': '/config-worker.json',
|
||||
'dev-worker-local': '/config-worker-local.json'
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ const mode = process.env.NODE_ENV || 'development'
|
||||
const prod = mode === 'production'
|
||||
const clientType = process.env.CLIENT_TYPE ?? ''
|
||||
const devServer = clientType === 'dev-server'
|
||||
const devServerTest = clientType === 'dev-server-test'
|
||||
const devServerWorker = clientType === 'dev-worker'
|
||||
const devServerWorkerLocal = clientType === 'dev-worker-local'
|
||||
const devProduction = clientType === 'dev-production'
|
||||
const devProductionHuly = clientType === 'dev-huly'
|
||||
const devProductionBold = clientType === 'dev-bold'
|
||||
const dev =
|
||||
(process.env.CLIENT_TYPE ?? '') === 'dev' || devServer || devProduction || devProductionHuly || devProductionBold || devServerWorker || devServerWorkerLocal
|
||||
(process.env.CLIENT_TYPE ?? '') === 'dev' || devServer || devProduction || devProductionHuly || devProductionBold || devServerWorker || devServerWorkerLocal || devServerTest
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
||||
|
||||
const { EsbuildPlugin } = require('esbuild-loader')
|
||||
@@ -71,6 +72,36 @@ const devProxy = {
|
||||
}
|
||||
}
|
||||
|
||||
const devProxyTest = {
|
||||
'/account': {
|
||||
target: 'http://localhost:3003',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/account': '' },
|
||||
logLevel: 'debug'
|
||||
},
|
||||
'/files': {
|
||||
target: 'http://localhost:8083',
|
||||
changeOrigin: true,
|
||||
logLevel: 'debug'
|
||||
},
|
||||
'/api/v1': {
|
||||
target: 'http://localhost:8083',
|
||||
changeOrigin: true,
|
||||
logLevel: 'debug'
|
||||
},
|
||||
'/import': {
|
||||
target: 'http://localhost:8083',
|
||||
changeOrigin: true,
|
||||
logLevel: 'debug'
|
||||
},
|
||||
'/rekoni/recognize': {
|
||||
target: 'http://localhost:4004',
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/rekoni/recognize': '/recognize' },
|
||||
logLevel: 'debug'
|
||||
}
|
||||
}
|
||||
|
||||
const devHulyProxy = {
|
||||
'/account': {
|
||||
target: 'https://account.huly.app/',
|
||||
@@ -148,6 +179,7 @@ const proxy = {
|
||||
'dev-worker': devProxy,
|
||||
'dev-worker-local': devProxy,
|
||||
'dev-server': devProxy,
|
||||
'dev-server-test': devProxyTest,
|
||||
'dev-production': devFrontProxy,
|
||||
'dev-bold': devBoldProxy,
|
||||
'dev-huly': devHulyProxy
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd ./pods/account
|
||||
rushx bundle
|
||||
|
||||
port=${1:-3000}
|
||||
echo "Running account on port: ${port}"
|
||||
#MONGO_URL=mongodb://localhost:27017,
|
||||
export DB_URL="mongodb://localhost:27018"
|
||||
# DB_URL=postgresql://postgres:example@localhost:5432,
|
||||
# DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable,
|
||||
export SERVER_SECRET="secret"
|
||||
export REGION_INFO="|America;europe|"
|
||||
export TRANSACTOR_URL="ws://transactor:3334;ws://localhost:3334,ws://transactor-europe:3335;ws://localhost:3335;europe,"
|
||||
export ACCOUNTS_URL="http://localhost:${port}"
|
||||
export ACCOUNT_PORT=${port}
|
||||
export FRONT_URL="http://localhost:8080"
|
||||
export STATS_URL="http://host.docker.internal:4900"
|
||||
export SES_URL=
|
||||
export MINIO_ACCESS_KEY="minioadmin"
|
||||
export MINIO_SECRET_KEY="minioadmin"
|
||||
export MINIO_ENDPOINT="localhost"
|
||||
export ADMIN_EMAILS=admin
|
||||
# DISABLE_SIGNUP=true,
|
||||
# INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml,
|
||||
# INIT_WORKSPACE=onboarding,
|
||||
node --inspect bundle/bundle.js
|
||||
@@ -589,8 +589,6 @@ export function devTool (
|
||||
// })
|
||||
// },
|
||||
// cmd.region,
|
||||
// true,
|
||||
// true,
|
||||
// 5000, // 5 gigabytes per blob
|
||||
// sharedPipelineContextVars,
|
||||
// async (storage, workspaceStorage) => {
|
||||
@@ -661,8 +659,6 @@ export function devTool (
|
||||
// })
|
||||
// },
|
||||
// cmd.region,
|
||||
// false,
|
||||
// false,
|
||||
// 100,
|
||||
// sharedPipelineContextVars
|
||||
// )
|
||||
@@ -859,8 +855,6 @@ export function devTool (
|
||||
// )
|
||||
// .option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 15mb)', '15')
|
||||
// .option('-f, --force', 'Force backup', false)
|
||||
// .option('-f, --fresh', 'Force fresh backup', false)
|
||||
// .option('-c, --clean', 'Force clean of old backup files, only with fresh backup option', false)
|
||||
// .option('-t, --timeout <timeout>', 'Connect timeout in seconds', '30')
|
||||
// .action(
|
||||
// async (
|
||||
@@ -869,8 +863,6 @@ export function devTool (
|
||||
// cmd: {
|
||||
// skip: string
|
||||
// force: boolean
|
||||
// fresh: boolean
|
||||
// clean: boolean
|
||||
// timeout: string
|
||||
// include: string
|
||||
// blobLimit: string
|
||||
@@ -882,8 +874,6 @@ export function devTool (
|
||||
// const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external')
|
||||
// await backup(toolCtx, endpoint, wsIds, storage, {
|
||||
// force: cmd.force,
|
||||
// freshBackup: cmd.fresh,
|
||||
// clean: cmd.clean,
|
||||
// include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())),
|
||||
// skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()),
|
||||
// timeout: 0,
|
||||
|
||||
@@ -150,6 +150,7 @@ export default plugin(platformId, {
|
||||
AccountNotConfirmed: '' as StatusCode,
|
||||
WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>,
|
||||
WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>,
|
||||
WorkspaceMigration: '' as StatusCode<{ workspaceUuid: string }>,
|
||||
SocialIdNotFound: '' as StatusCode<{ socialId: string, type: string }>,
|
||||
SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>,
|
||||
SocialIdAlreadyConfirmed: '' as StatusCode<{ socialId: string, type: string }>,
|
||||
|
||||
@@ -283,6 +283,9 @@ class Connection implements ClientConnection {
|
||||
if (resp.error?.code === platform.status.WorkspaceArchived) {
|
||||
this.opt?.onArchived?.()
|
||||
}
|
||||
if (resp.error?.code === platform.status.WorkspaceMigration) {
|
||||
this.opt?.onMigration?.()
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.id !== undefined) {
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface ClientFactoryOptions {
|
||||
onUpgrade?: () => void
|
||||
onUnauthorized?: () => void
|
||||
onArchived?: () => void
|
||||
onMigration?: () => void
|
||||
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>
|
||||
ctx?: MeasureContext
|
||||
onDialTimeout?: () => void | Promise<void>
|
||||
|
||||
@@ -27,11 +27,13 @@
|
||||
Scroller,
|
||||
SearchEdit,
|
||||
showPopup,
|
||||
ticker
|
||||
ticker,
|
||||
Grid
|
||||
} from '@hcengineering/ui'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation } from '../utils'
|
||||
import { RegionInfo } from '@hcengineering/account-client'
|
||||
import ColStar from '@hcengineering/ui/src/components/icons/ColStar.svelte'
|
||||
|
||||
$: now = $ticker
|
||||
|
||||
@@ -44,7 +46,7 @@
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
type WorkspaceInfo = WorkspaceInfoWithStatus & { attempts: number }
|
||||
type WorkspaceInfo = WorkspaceInfoWithStatus & { processingAttempts: number }
|
||||
|
||||
let workspaces: WorkspaceInfo[] = []
|
||||
|
||||
@@ -193,13 +195,19 @@
|
||||
|
||||
let selectedRegionId: string = ''
|
||||
void getRegionInfo().then((_regionInfo) => {
|
||||
regionInfo = _regionInfo?.filter((it) => it.name !== '') ?? []
|
||||
regionInfo = _regionInfo ?? []
|
||||
if (selectedRegionId === '' && regionInfo.length > 0) {
|
||||
selectedRegionId = regionInfo[0].region
|
||||
}
|
||||
})
|
||||
|
||||
$: selectedRegionName = regionInfo.find((it) => it.region === selectedRegionId)?.name
|
||||
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
|
||||
$: selectedRegionName =
|
||||
selectedRegionRef !== undefined
|
||||
? selectedRegionRef.name.length > 0
|
||||
? selectedRegionRef.name
|
||||
: selectedRegionRef.region
|
||||
: ''
|
||||
|
||||
$: byVersion = groupByArray(
|
||||
workspaces.filter((it) => {
|
||||
@@ -278,8 +286,11 @@
|
||||
<ButtonMenu
|
||||
selected={selectedRegionId}
|
||||
autoSelectionIfOne
|
||||
title={regionInfo.find((it) => it.region === selectedRegionId)?.name}
|
||||
items={regionInfo.map((it) => ({ id: it.region === '' ? '#' : it.region, label: getEmbeddedLabel(it.name) }))}
|
||||
title={selectedRegionName}
|
||||
items={regionInfo.map((it) => ({
|
||||
id: it.region === '' ? '#' : it.region,
|
||||
label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
|
||||
}))}
|
||||
on:selected={(it) => {
|
||||
selectedRegionId = it.detail === '#' ? '' : it.detail
|
||||
}}
|
||||
@@ -291,12 +302,13 @@
|
||||
{#each Object.keys(dayRanges) as k}
|
||||
{@const v = groupped.get(k) ?? []}
|
||||
{@const hasMore = (groupped.get(k) ?? []).length > limit}
|
||||
{@const activeV = v.filter((it) => it.mode === 'active' && (it.region ?? '') !== selectedRegionId)}
|
||||
{@const archivedV = v.filter((it) => it.mode === 'archived')}
|
||||
{@const deletedV = v.filter((it) => it.mode === 'deleted')}
|
||||
{@const av = v.length - archivedV.length - deletedV.length}
|
||||
{@const activeV = v.filter((it) => isActiveMode(it.mode) && it.region !== selectedRegionId)}
|
||||
{@const activeAll = v.filter((it) => isActiveMode(it.mode))}
|
||||
{@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
|
||||
{@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
|
||||
{@const maintenance = v.length - activeAll.length - archivedV.length - deletedV.length}
|
||||
{#if v.length > 0}
|
||||
<Expandable expandable={true} bordered={true}>
|
||||
<Expandable expandable={true} bordered={true} expanded={search.trim().length > 0}>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="fs-title focused-button flex-row-center">
|
||||
{k} -
|
||||
@@ -305,8 +317,8 @@
|
||||
{:else}
|
||||
{v.length}
|
||||
{/if}
|
||||
{#if av > 0}
|
||||
- maitenance: {av}
|
||||
{#if maintenance > 0}
|
||||
- maitenance: {maintenance}
|
||||
{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
@@ -324,18 +336,18 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="tools">
|
||||
{#if archivedV.length > 0}
|
||||
{#if activeAll.length > 0}
|
||||
<Button
|
||||
icon={IconStop}
|
||||
label={getEmbeddedLabel(`Mass Archive ${archivedV.length}`)}
|
||||
label={getEmbeddedLabel(`Mass Archive ${activeAll.length}`)}
|
||||
kind={'ghost'}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Mass Archive ${archivedV.length}`),
|
||||
message: getEmbeddedLabel(`Please confirm archive ${archivedV.length} workspaces`),
|
||||
label: getEmbeddedLabel(`Mass Archive ${activeAll.length}`),
|
||||
message: getEmbeddedLabel(`Please confirm archive ${activeAll.length} workspaces`),
|
||||
action: async () => {
|
||||
void performWorkspaceOperation(
|
||||
archivedV.map((it) => it.uuid),
|
||||
activeAll.map((it) => it.uuid),
|
||||
'archive'
|
||||
)
|
||||
}
|
||||
@@ -351,8 +363,8 @@
|
||||
label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Mass Migrate ${archivedV.length}`),
|
||||
message: getEmbeddedLabel(`Please confirm migrate ${archivedV.length} workspaces`),
|
||||
label: getEmbeddedLabel(`Mass Migrate ${activeV.length}`),
|
||||
message: getEmbeddedLabel(`Please confirm migrate ${activeV.length} workspaces`),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(
|
||||
activeV.map((it) => it.uuid),
|
||||
@@ -371,38 +383,36 @@
|
||||
{@const bIdx = backupIdx.get(workspace.uuid)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex fs-title cursor-pointer focused-button bordered">
|
||||
<div class="flex p-2">
|
||||
<span class="label overflow-label flex-row-center" style:width={'12rem'}>
|
||||
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.uuid}`}>
|
||||
<Grid column={10} equalHeight={false} columnGap={0}>
|
||||
<span class="label overflow-label flex-row-center flex-grow flex-shrink">
|
||||
{wsName}
|
||||
<div class="ml-1">
|
||||
<Button icon={IconOpen} size={'small'} on:click={() => select(workspace.url)} />
|
||||
</div>
|
||||
</span>
|
||||
<div class="ml-1" style:width={'12rem'}>
|
||||
{workspace.createdBy}
|
||||
<div class="ml-1">
|
||||
<!-- Retrieve created by-->
|
||||
<!-- {workspace.createdBy} -->
|
||||
</div>
|
||||
<span class="label overflow-label" style:width={'8rem'}>
|
||||
<span class="label overflow-label flex-shrink p-1">
|
||||
{workspace.region ?? ''}
|
||||
</span>
|
||||
<span class="label overflow-label" style:width={'5rem'}>
|
||||
<span class="label overflow-label flex-shrink p-1">
|
||||
{lastUsageDays} days
|
||||
</span>
|
||||
|
||||
<span class="label overflow-label" style:width={'10rem'}>
|
||||
<span class="label overflow-label p-1">
|
||||
{workspace.mode ?? '-'}
|
||||
</span>
|
||||
|
||||
<span class="label overflow-label" style:width={'2rem'}>
|
||||
{workspace.attempts}
|
||||
<span class="label overflow-label flex-shrink">
|
||||
{workspace.processingAttempts}
|
||||
</span>
|
||||
|
||||
<span class="flex flex-between" style:width={'5rem'}>
|
||||
<span class="flex flex-shrink">
|
||||
{#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
|
||||
({workspace.processingProgress}%)
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex flex-between" style:width={'5rem'}>
|
||||
<span class="flex flex-shrink">
|
||||
{#if workspace.backupInfo != null}
|
||||
{@const sz = Math.max(
|
||||
workspace.backupInfo.backupSize,
|
||||
@@ -419,7 +429,7 @@
|
||||
[#{bIdx}]
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex flex-between" style:width={'5rem'}>
|
||||
<span class="flex flex-shrink">
|
||||
{#if workspace.backupInfo != null}
|
||||
{@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))}
|
||||
|
||||
@@ -430,82 +440,81 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-grow gap-1-5 flex-between">
|
||||
<div class="flex flex-row-center gap-1-5">
|
||||
{#if workspace.mode === 'active'}
|
||||
<Button
|
||||
icon={IconStop}
|
||||
size={'small'}
|
||||
label={getEmbeddedLabel('Archive')}
|
||||
kind={'ghost'}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Archive ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'archive')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-grow gap-1-5 flex-shrink">
|
||||
<div class="flex flex-row-center gap-1-5">
|
||||
{#if workspace.mode === 'active'}
|
||||
<Button
|
||||
icon={IconStop}
|
||||
size={'small'}
|
||||
label={getEmbeddedLabel('Archive')}
|
||||
kind={'ghost'}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Archive ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'archive')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if workspace.mode === 'archived'}
|
||||
<Button
|
||||
icon={IconStart}
|
||||
size={'small'}
|
||||
kind={'ghost'}
|
||||
label={getEmbeddedLabel('Unarchive')}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'unarchive')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if workspace.mode === 'archived'}
|
||||
<Button
|
||||
icon={IconStart}
|
||||
size={'small'}
|
||||
kind={'ghost'}
|
||||
label={getEmbeddedLabel('Unarchive')}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'unarchive')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
|
||||
<Button
|
||||
icon={IconArrowRight}
|
||||
size={'small'}
|
||||
kind={'positive'}
|
||||
label={getEmbeddedLabel('Migrate')}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Migrate ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
|
||||
<Button
|
||||
icon={IconArrowRight}
|
||||
size={'small'}
|
||||
kind={'positive'}
|
||||
label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Migrate ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
|
||||
<Button
|
||||
icon={IconStop}
|
||||
size={'small'}
|
||||
kind={'dangerous'}
|
||||
label={getEmbeddedLabel('Delete')}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Delete ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'delete')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
|
||||
<Button
|
||||
icon={IconStop}
|
||||
size={'small'}
|
||||
kind={'dangerous'}
|
||||
label={getEmbeddedLabel('Delete')}
|
||||
on:click={() => {
|
||||
showPopup(MessageBox, {
|
||||
label: getEmbeddedLabel(`Delete ${workspace.url}`),
|
||||
message: getEmbeddedLabel('Please confirm'),
|
||||
action: async () => {
|
||||
await performWorkspaceOperation(workspace.uuid, 'delete')
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
{/each}
|
||||
</Expandable>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Otevřít v postranním panelu na nové kartě",
|
||||
"ConfigureWidgets": "Nastavit widgety",
|
||||
"Tab": "Karta",
|
||||
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení..."
|
||||
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení...",
|
||||
"WorkspaceIsMigrating": "Pracovní prostor je aktualizován. Prosím čekejte..."
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "In neuer Seitenleiste öffnen",
|
||||
"ConfigureWidgets": "Widgets konfigurieren",
|
||||
"Tab": "Tab",
|
||||
"WorkspaceIsArchived": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung..."
|
||||
"WorkspaceIsArchived": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung...",
|
||||
"WorkspaceIsMigrating": "Workspace wird aktualisiert. Bitte warten..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Open in sidebar new tab",
|
||||
"ConfigureWidgets": "Configure widgets",
|
||||
"Tab": "Tab",
|
||||
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore..."
|
||||
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore...",
|
||||
"WorkspaceIsMigrating": "Workspace is being updated. Please wait..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
|
||||
"ConfigureWidgets": "Configurar widgets",
|
||||
"Tab": "Pestaña",
|
||||
"WorkspaceIsArchived": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo..."
|
||||
"WorkspaceIsArchived": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo...",
|
||||
"WorkspaceIsMigrating": "El espacio de trabajo se está actualizando. Por favor, espere..."
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
|
||||
"ConfigureWidgets": "Configurer les widgets",
|
||||
"Tab": "Onglet",
|
||||
"WorkspaceIsArchived": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer..."
|
||||
"WorkspaceIsArchived": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer...",
|
||||
"WorkspaceIsMigrating": "L'espace de travail est en cours de mise à jour. Veuillez patienter..."
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"MobileNotSupported": "Spiacenti, il supporto per i dispositivi mobili arriverà presto. Nel frattempo, si prega di utilizzare un computer desktop",
|
||||
"LogInAnyway": "Accedi comunque",
|
||||
"WorkspaceCreating": "Creazione in corso...",
|
||||
"AccessDenied": "L'oggetto non esiste o non hai autorizzazione per accedervi."
|
||||
"AccessDenied": "L'oggetto non esiste o non hai autorizzazione per accedervi.",
|
||||
"WorkspaceIsMigrating": "Il workspace è in fase di aggiornamento. Attendi..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
|
||||
"ConfigureWidgets": "Configurar widgets",
|
||||
"Tab": "Aba",
|
||||
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo..."
|
||||
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo...",
|
||||
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde..."
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
|
||||
"ConfigureWidgets": "Настроить виджеты",
|
||||
"Tab": "Вкладка",
|
||||
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..."
|
||||
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления...",
|
||||
"WorkspaceIsMigrating": "Рабочее пространство обновляется. Пожалуйста, подождите..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
|
||||
"ConfigureWidgets": "配置小部件",
|
||||
"Tab": "选项卡",
|
||||
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复..."
|
||||
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复...",
|
||||
"WorkspaceIsMigrating": "工作区正在更新。请稍候..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +213,9 @@
|
||||
<div class="p-3">2.</div>
|
||||
<Button
|
||||
icon={IconArrowRight}
|
||||
label={getEmbeddedLabel('Reboot server')}
|
||||
label={getEmbeddedLabel('Reboot workspace')}
|
||||
on:click={() => {
|
||||
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=reboot`, {
|
||||
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -243,6 +243,17 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
onArchived: () => {
|
||||
translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => {
|
||||
versionError.set(r)
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 5000)
|
||||
})
|
||||
},
|
||||
onMigration: () => {
|
||||
translateCB(plugin.string.WorkspaceIsMigrating, {}, get(themeStore).language, (r) => {
|
||||
versionError.set(r)
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 5000)
|
||||
})
|
||||
},
|
||||
// We need to refresh all active live queries and clear old queries.
|
||||
|
||||
@@ -256,7 +256,8 @@ export default plugin(workbenchId, {
|
||||
OpenInSidebar: '' as IntlString,
|
||||
OpenInSidebarNewTab: '' as IntlString,
|
||||
ConfigureWidgets: '' as IntlString,
|
||||
WorkspaceIsArchived: '' as IntlString
|
||||
WorkspaceIsArchived: '' as IntlString,
|
||||
WorkspaceIsMigrating: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
Search: '' as Asset
|
||||
|
||||
@@ -1140,6 +1140,11 @@
|
||||
"projectFolder": "tests/sanity",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/tests-ws-sanity",
|
||||
"projectFolder": "ws-tests/sanity",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/qms-tests-sanity",
|
||||
"projectFolder": "qms-tests/sanity",
|
||||
|
||||
@@ -241,7 +241,6 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection<WorkspaceS
|
||||
|
||||
for (const key of Object.keys(query)) {
|
||||
const qVal = (query as any)[key]
|
||||
|
||||
if (key === 'workspaceUuid') {
|
||||
res.uuid = qVal
|
||||
} else {
|
||||
|
||||
@@ -15,22 +15,22 @@
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import {
|
||||
AccountRole,
|
||||
concatLink,
|
||||
Data,
|
||||
isActiveMode,
|
||||
isWorkspaceCreating,
|
||||
MeasureContext,
|
||||
SocialIdType,
|
||||
systemAccountUuid,
|
||||
Version,
|
||||
SocialIdType,
|
||||
type BackupStatus,
|
||||
type Branding,
|
||||
AccountRole,
|
||||
type Person,
|
||||
type WorkspaceMemberInfo,
|
||||
type PersonUuid,
|
||||
isActiveMode,
|
||||
type WorkspaceUuid,
|
||||
type WorkspaceMode
|
||||
type WorkspaceMemberInfo,
|
||||
type WorkspaceMode,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, {
|
||||
getMetadata,
|
||||
@@ -45,48 +45,48 @@ import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
||||
import { accountPlugin } from './plugin'
|
||||
import type {
|
||||
AccountDB,
|
||||
AccountMethodHandler,
|
||||
LoginInfo,
|
||||
OtpInfo,
|
||||
RegionInfo,
|
||||
WorkspaceEvent,
|
||||
WorkspaceOperation,
|
||||
AccountMethodHandler,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceStatus,
|
||||
SocialId,
|
||||
Workspace
|
||||
Workspace,
|
||||
WorkspaceEvent,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceOperation,
|
||||
WorkspaceStatus
|
||||
} from './types'
|
||||
import {
|
||||
checkInvite,
|
||||
cleanEmail,
|
||||
EndpointKind,
|
||||
getEndpoint,
|
||||
getRegions,
|
||||
verifyPassword,
|
||||
isOtpValid,
|
||||
confirmEmail,
|
||||
createAccount,
|
||||
createWorkspaceRecord,
|
||||
doJoinByInvite,
|
||||
EndpointKind,
|
||||
getAccount,
|
||||
getEmailSocialId,
|
||||
getEndpoint,
|
||||
getFrontUrl,
|
||||
getRegions,
|
||||
getRolePower,
|
||||
getSesUrl,
|
||||
getSocialIdByKey,
|
||||
getWorkspaceById,
|
||||
getWorkspaceInfoWithStatusById,
|
||||
getWorkspaceInvite,
|
||||
getWorkspaces,
|
||||
getWorkspacesInfoWithStatusByIds,
|
||||
GUEST_ACCOUNT,
|
||||
isOtpValid,
|
||||
selectWorkspace,
|
||||
sendEmailConfirmation,
|
||||
sendOtp,
|
||||
setPassword,
|
||||
wrap,
|
||||
createWorkspaceRecord,
|
||||
checkInvite,
|
||||
GUEST_ACCOUNT,
|
||||
getWorkspaceById,
|
||||
getWorkspaceInvite,
|
||||
getRolePower,
|
||||
sendEmailConfirmation,
|
||||
getEmailSocialId,
|
||||
confirmEmail,
|
||||
getAccount,
|
||||
getSesUrl,
|
||||
getFrontUrl,
|
||||
getWorkspaceInfoWithStatusById,
|
||||
signUpByEmail,
|
||||
selectWorkspace,
|
||||
doJoinByInvite,
|
||||
getWorkspacesInfoWithStatusByIds,
|
||||
getSocialIdByKey,
|
||||
getWorkspaces
|
||||
verifyPassword,
|
||||
wrap
|
||||
} from './utils'
|
||||
|
||||
// Move to config?
|
||||
@@ -857,7 +857,7 @@ export async function performWorkspaceOperation (
|
||||
): Promise<boolean> {
|
||||
const { extra } = decodeTokenVerbose(ctx, token)
|
||||
|
||||
if (extra?.service !== 'admin') {
|
||||
if (extra?.admin !== 'true') {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
@@ -874,7 +874,7 @@ export async function performWorkspaceOperation (
|
||||
switch (event) {
|
||||
case 'delete':
|
||||
if (workspace.status.mode !== 'active') {
|
||||
throw new PlatformError(unknownError('Archive allowed only for active workspaces'))
|
||||
throw new PlatformError(unknownError('Delete allowed only for active workspaces'))
|
||||
}
|
||||
|
||||
update.mode = 'pending-deletion'
|
||||
@@ -883,7 +883,7 @@ export async function performWorkspaceOperation (
|
||||
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
|
||||
break
|
||||
case 'archive':
|
||||
if (isActiveMode(workspace.status.mode)) {
|
||||
if (!isActiveMode(workspace.status.mode)) {
|
||||
throw new PlatformError(unknownError('Archiving allowed only for active workspaces'))
|
||||
}
|
||||
|
||||
@@ -921,7 +921,7 @@ export async function performWorkspaceOperation (
|
||||
|
||||
update.mode = 'migration-pending-backup'
|
||||
// NOTE: will only work for Mongo accounts
|
||||
;(update as any).targetRegion = params[0]
|
||||
update.targetRegion = params[0]
|
||||
update.processingAttempts = 0
|
||||
update.processingProgress = 0
|
||||
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
|
||||
@@ -1278,7 +1278,7 @@ export async function updateWorkspaceInfo (
|
||||
update.processingProgress = progress
|
||||
break
|
||||
case 'migrate-clean-done':
|
||||
wsUpdate.region = (workspace as any).targetRegion ?? ''
|
||||
wsUpdate.region = workspace.status.targetRegion ?? ''
|
||||
update.mode = 'pending-restore'
|
||||
update.processingProgress = progress
|
||||
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
|
||||
@@ -1323,8 +1323,8 @@ export async function updateWorkspaceInfo (
|
||||
await db.workspaceStatus.updateOne(
|
||||
{ workspaceUuid: workspace.uuid },
|
||||
{
|
||||
...update,
|
||||
lastProcessingTime: Date.now()
|
||||
lastProcessingTime: Date.now(), // Some operations override it.
|
||||
...update
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface WorkspaceStatus {
|
||||
processingAttempts?: number
|
||||
processingMessage?: string
|
||||
backupInfo?: BackupStatus
|
||||
|
||||
targetRegion?: string
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
systemAccountUuid,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
|
||||
isActiveMode,
|
||||
isArchivingMode,
|
||||
isMigrationMode,
|
||||
type PersonUuid
|
||||
} from '@hcengineering/core'
|
||||
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
|
||||
@@ -560,19 +558,6 @@ export async function selectWorkspace (
|
||||
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUrl }))
|
||||
}
|
||||
|
||||
if (isArchivingMode(wsStatus.mode) || isMigrationMode(wsStatus.mode)) {
|
||||
const result: WorkspaceLoginInfo = {
|
||||
account: accountUuid,
|
||||
endpoint: '',
|
||||
token: generateToken(accountUuid, workspace.uuid),
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
workspaceDataId: workspace.dataId,
|
||||
role
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -92,8 +92,6 @@ export async function backupWorkspace (
|
||||
externalStorage: StorageAdapter
|
||||
) => DbConfiguration,
|
||||
region: string,
|
||||
freshBackup: boolean = false,
|
||||
clean: boolean = false,
|
||||
downloadLimit: number,
|
||||
contextVars: Record<string, any>,
|
||||
|
||||
@@ -129,8 +127,6 @@ export async function backupWorkspace (
|
||||
return getConfig(ctx, mainDbUrl, workspace, branding, externalStorage)
|
||||
},
|
||||
region,
|
||||
freshBackup,
|
||||
clean,
|
||||
downloadLimit,
|
||||
[],
|
||||
contextVars
|
||||
|
||||
@@ -672,8 +672,6 @@ export async function backup (
|
||||
include?: Set<string>
|
||||
skipDomains: string[]
|
||||
force: boolean
|
||||
freshBackup: boolean // If passed as true, will download all documents except blobs as new backup
|
||||
clean: boolean // If set will perform a clena of old backup files
|
||||
timeout: number
|
||||
connectTimeout: number
|
||||
skipBlobContentTypes: string[]
|
||||
@@ -687,8 +685,6 @@ export async function backup (
|
||||
token?: string
|
||||
} = {
|
||||
force: false,
|
||||
freshBackup: false,
|
||||
clean: false,
|
||||
timeout: 0,
|
||||
skipDomains: [],
|
||||
connectTimeout: 30000,
|
||||
@@ -706,7 +702,6 @@ export async function backup (
|
||||
ctx = ctx.newChild('backup', {
|
||||
workspaceId,
|
||||
force: options.force,
|
||||
recheck: options.freshBackup,
|
||||
timeout: options.timeout
|
||||
})
|
||||
|
||||
@@ -767,7 +762,7 @@ export async function backup (
|
||||
|
||||
let lastTxChecked = false
|
||||
// Skip backup if there is no transaction changes.
|
||||
if (options.getLastTx !== undefined && !options.freshBackup) {
|
||||
if (options.getLastTx !== undefined) {
|
||||
lastTx = await options.getLastTx()
|
||||
if (lastTx !== undefined) {
|
||||
if (lastTx._id === backupInfo.lastTxId && !options.force) {
|
||||
@@ -793,7 +788,7 @@ export async function backup (
|
||||
? await options.getConnection()
|
||||
: ((await createClient(transactorUrl, token, undefined, options.connectTimeout)) as CoreClient & BackupClient)
|
||||
|
||||
if (!lastTxChecked && !options.freshBackup) {
|
||||
if (!lastTxChecked) {
|
||||
lastTx = await connection.findOne(
|
||||
core.class.Tx,
|
||||
{ objectSpace: { $ne: core.space.Model } },
|
||||
@@ -932,7 +927,7 @@ export async function backup (
|
||||
if (digest.delete(id as Ref<Doc>)) {
|
||||
oldHash.set(id as Ref<Doc>, currentHash)
|
||||
}
|
||||
if (currentHash !== serverDocHash || (options.freshBackup && domain !== DOMAIN_BLOB)) {
|
||||
if (currentHash !== serverDocHash) {
|
||||
if (changes.updated.has(id as Ref<Doc>)) {
|
||||
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
|
||||
}
|
||||
@@ -1355,36 +1350,6 @@ export async function backup (
|
||||
if (!canceled()) {
|
||||
backupInfo.lastTxId = lastTx?._id ?? '0' // We could store last tx, since full backup is complete
|
||||
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
|
||||
|
||||
if (options.freshBackup && options.clean) {
|
||||
// Preparing a list of files to clean
|
||||
|
||||
ctx.info('Cleaning old backup files...')
|
||||
for (const sn of backupInfo.snapshots.slice(0, backupInfo.snapshots.length - 1)) {
|
||||
const filesToDelete: string[] = []
|
||||
for (const [domain, dsn] of [...Object.entries(sn.domains)]) {
|
||||
if (domain === DOMAIN_BLOB) {
|
||||
continue
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete (sn.domains as any)[domain]
|
||||
|
||||
filesToDelete.push(...(dsn.snapshots ?? []))
|
||||
filesToDelete.push(...(dsn.storage ?? []))
|
||||
if (dsn.snapshot !== undefined) {
|
||||
filesToDelete.push(dsn.snapshot)
|
||||
}
|
||||
}
|
||||
for (const file of filesToDelete) {
|
||||
ctx.info('Removing file...', { file })
|
||||
await storage.delete(file)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete sizeInfo[file]
|
||||
}
|
||||
}
|
||||
ctx.info('Cleaning complete...')
|
||||
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
|
||||
}
|
||||
}
|
||||
|
||||
const addFileSize = async (file: string | undefined | null): Promise<void> => {
|
||||
@@ -1735,7 +1700,7 @@ export async function restore (
|
||||
try {
|
||||
let serverEndpoint = await getTransactorEndpoint(token, 'external')
|
||||
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
|
||||
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${workspaceId}`, {
|
||||
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -73,8 +73,6 @@ class BackupWorker {
|
||||
) => DbConfiguration,
|
||||
readonly region: string,
|
||||
readonly contextVars: Record<string, any>,
|
||||
readonly freshWorkspace: boolean = false,
|
||||
readonly clean: boolean = false,
|
||||
readonly skipDomains: string[] = []
|
||||
) {}
|
||||
|
||||
@@ -282,8 +280,6 @@ class BackupWorker {
|
||||
backup(ctx, '', wsIds, storage, {
|
||||
skipDomains: this.skipDomains,
|
||||
force: true,
|
||||
freshBackup: this.freshWorkspace,
|
||||
clean: this.clean,
|
||||
timeout: this.config.Timeout * 1000,
|
||||
connectTimeout: 5 * 60 * 1000, // 5 minutes to,
|
||||
blobDownloadLimit: this.downloadLimit,
|
||||
@@ -408,8 +404,6 @@ export async function doBackupWorkspace (
|
||||
externalStorage: StorageAdapter
|
||||
) => DbConfiguration,
|
||||
region: string,
|
||||
freshWorkspace: boolean,
|
||||
clean: boolean,
|
||||
downloadLimit: number,
|
||||
skipDomains: string[],
|
||||
contextVars: Record<string, any>,
|
||||
@@ -423,8 +417,6 @@ export async function doBackupWorkspace (
|
||||
getConfig,
|
||||
region,
|
||||
contextVars,
|
||||
freshWorkspace,
|
||||
clean,
|
||||
skipDomains
|
||||
)
|
||||
backupWorker.downloadLimit = downloadLimit
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
import {
|
||||
type Account,
|
||||
type PersonId,
|
||||
type Branding,
|
||||
type Class,
|
||||
type Doc,
|
||||
@@ -29,6 +28,8 @@ import {
|
||||
type MeasureContext,
|
||||
type ModelDb,
|
||||
type Obj,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
@@ -39,10 +40,9 @@ import {
|
||||
type Tx,
|
||||
type TxFactory,
|
||||
type TxResult,
|
||||
type WorkspaceUuid,
|
||||
type WorkspaceIds,
|
||||
type WorkspaceDataId,
|
||||
type PersonUuid
|
||||
type WorkspaceIds,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, Resource } from '@hcengineering/platform'
|
||||
import type { LiveQuery } from '@hcengineering/query'
|
||||
@@ -649,8 +649,8 @@ export interface AddSessionActive {
|
||||
}
|
||||
export type AddSessionResponse =
|
||||
| AddSessionActive
|
||||
| { upgrade: true }
|
||||
| { error: any, terminate?: boolean, archived?: boolean }
|
||||
| { upgrade: true, progress?: number }
|
||||
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
|
||||
|
||||
export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
SessionManager,
|
||||
StorageAdapter,
|
||||
type SessionFactory,
|
||||
type AddSessionResponse,
|
||||
type ClientSessionCtx,
|
||||
type ConnectionSocket,
|
||||
type Session,
|
||||
@@ -355,11 +356,7 @@ class TSessionManager implements SessionManager {
|
||||
rawToken: string,
|
||||
pipelineFactory: PipelineFactory,
|
||||
sessionId: string | undefined
|
||||
): Promise<
|
||||
| { session: Session, context: MeasureContext, workspaceId: WorkspaceUuid }
|
||||
| { upgrade: true, progress?: number }
|
||||
| { error: any, terminate?: boolean, archived?: boolean }
|
||||
> {
|
||||
): Promise<AddSessionResponse> {
|
||||
const { workspace: workspaceUuid } = token
|
||||
|
||||
let workspaceInfo: WorkspaceInfoWithStatus | undefined
|
||||
@@ -376,15 +373,15 @@ class TSessionManager implements SessionManager {
|
||||
|
||||
if (isArchivingMode(workspaceInfo.mode)) {
|
||||
// No access to disabled workspaces for regular users
|
||||
return { error: new Error('Workspace is archived'), terminate: true, archived: true }
|
||||
return { error: new Error('Workspace is archived'), terminate: true, specialError: 'archived' }
|
||||
}
|
||||
if (isMigrationMode(workspaceInfo.mode)) {
|
||||
// No access to disabled workspaces for regular users
|
||||
return { error: new Error('Workspace is in region migration'), terminate: true, archived: false }
|
||||
return { error: new Error('Workspace is in region migration'), terminate: true, specialError: 'migration' }
|
||||
}
|
||||
if (isRestoringMode(workspaceInfo.mode)) {
|
||||
// No access to disabled workspaces for regular users
|
||||
return { error: new Error('Workspace is in backup restore'), terminate: true, archived: false }
|
||||
return { error: new Error('Workspace is in backup restore'), terminate: true, specialError: 'migration' }
|
||||
}
|
||||
|
||||
if (workspaceInfo.isDisabled === true && token.account !== systemAccountUuid && token.extra?.admin !== 'true') {
|
||||
@@ -836,11 +833,14 @@ class TSessionManager implements SessionManager {
|
||||
async forceClose (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket): Promise<void> {
|
||||
const ws = this.workspaces.get(wsId)
|
||||
if (ws !== undefined) {
|
||||
this.ctx.warn('force-close', { name: ws.workspaceName })
|
||||
ws.upgrade = true // We need to similare upgrade to refresh all clients.
|
||||
ws.closing = this.closeAll(wsId, ws, 99, 'force-close', ignoreSocket)
|
||||
this.workspaces.delete(wsId)
|
||||
await ws.closing
|
||||
ws.closing = undefined
|
||||
} else {
|
||||
this.ctx.warn('force-close-unknown', { wsId })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ export async function upgradeModel (
|
||||
const token = generateToken(systemAccountUuid, wsIds.uuid, { service: 'tool', admin: 'true' })
|
||||
|
||||
try {
|
||||
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${wsIds.uuid}`, {
|
||||
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -12,23 +12,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import {
|
||||
getBranding,
|
||||
isArchivingMode,
|
||||
isMigrationMode,
|
||||
isRestoringMode,
|
||||
systemAccountUuid,
|
||||
type BrandingMap,
|
||||
type Data,
|
||||
type MeasureContext,
|
||||
type Tx,
|
||||
type Version,
|
||||
type WorkspaceUpdateEvent,
|
||||
isArchivingMode,
|
||||
isMigrationMode,
|
||||
isRestoringMode,
|
||||
type WorkspaceInfoWithStatus,
|
||||
getBranding,
|
||||
systemAccountUuid
|
||||
type WorkspaceUpdateEvent,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { type MigrateOperation, type ModelLogger } from '@hcengineering/model'
|
||||
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import { withRetryConnUntilSuccess, withRetryConnUntilTimeout } from '@hcengineering/server-client'
|
||||
import {
|
||||
getTransactorEndpoint,
|
||||
withRetryConnUntilSuccess,
|
||||
withRetryConnUntilTimeout
|
||||
} from '@hcengineering/server-client'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { FileModelLogger, prepareTools } from '@hcengineering/server-tool'
|
||||
import path from 'path'
|
||||
@@ -420,6 +425,20 @@ export class WorkspaceWorker {
|
||||
}
|
||||
}
|
||||
|
||||
async sendTransactorMaitenance (token: string, ws: WorkspaceUuid): Promise<void> {
|
||||
try {
|
||||
let serverEndpoint = await getTransactorEndpoint(token)
|
||||
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
|
||||
console.log('sending event', serverEndpoint, ws)
|
||||
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private async doWorkspaceOperation (
|
||||
ctx: MeasureContext,
|
||||
workspace: WorkspaceInfoWithStatus,
|
||||
@@ -454,6 +473,8 @@ export class WorkspaceWorker {
|
||||
case 'archiving-pending-backup':
|
||||
case 'archiving-backup': {
|
||||
await sendEvent('archiving-backup-started', 0)
|
||||
|
||||
await this.sendTransactorMaitenance(token, workspace.uuid)
|
||||
if (await this.doBackup(ctx, workspace, opt, true)) {
|
||||
await sendEvent('archiving-backup-done', 100)
|
||||
}
|
||||
@@ -476,6 +497,7 @@ export class WorkspaceWorker {
|
||||
case 'deleting': {
|
||||
// We should remove DB, not storages.
|
||||
await sendEvent('delete-started', 0)
|
||||
await this.sendTransactorMaitenance(token, workspace.uuid)
|
||||
try {
|
||||
await this.doCleanup(ctx, workspace, true)
|
||||
} catch (err: any) {
|
||||
@@ -489,6 +511,7 @@ export class WorkspaceWorker {
|
||||
case 'migration-pending-backup':
|
||||
case 'migration-backup':
|
||||
await sendEvent('migrate-backup-started', 0)
|
||||
await this.sendTransactorMaitenance(token, workspace.uuid)
|
||||
if (await this.doBackup(ctx, workspace, opt, false)) {
|
||||
await sendEvent('migrate-backup-done', 100)
|
||||
}
|
||||
@@ -497,6 +520,7 @@ export class WorkspaceWorker {
|
||||
case 'migration-clean': {
|
||||
// We should remove DB, not storages.
|
||||
await sendEvent('migrate-clean-started', 0)
|
||||
await this.sendTransactorMaitenance(token, workspace.uuid)
|
||||
try {
|
||||
await this.doCleanup(ctx, workspace, false)
|
||||
} catch (err: any) {
|
||||
@@ -590,8 +614,6 @@ export class WorkspaceWorker {
|
||||
})
|
||||
},
|
||||
this.region,
|
||||
archive,
|
||||
archive,
|
||||
50000,
|
||||
['blob'],
|
||||
sharedPipelineContextVars,
|
||||
@@ -604,7 +626,7 @@ export class WorkspaceWorker {
|
||||
}
|
||||
)
|
||||
if (result) {
|
||||
console.log('backup completed')
|
||||
ctx.info('backup completed')
|
||||
return true
|
||||
}
|
||||
} finally {
|
||||
@@ -681,7 +703,7 @@ export class WorkspaceWorker {
|
||||
}
|
||||
)
|
||||
if (result) {
|
||||
console.log('backup completed')
|
||||
ctx.info('restore completed')
|
||||
return true
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -225,7 +225,7 @@ export function startHttpServer (
|
||||
}
|
||||
case 'force-close': {
|
||||
const wsId = req.query.wsId as WorkspaceUuid
|
||||
void sessions.forceClose(wsId)
|
||||
void sessions.forceClose(wsId ?? payload.workspace)
|
||||
res.writeHead(200)
|
||||
res.end()
|
||||
return
|
||||
@@ -433,7 +433,7 @@ export function startHttpServer (
|
||||
if (webSocketData.session instanceof Promise) {
|
||||
void webSocketData.session.then((s) => {
|
||||
if ('error' in s) {
|
||||
if (s.archived === true) {
|
||||
if (s.specialError === 'archived') {
|
||||
cs.send(
|
||||
ctx,
|
||||
{
|
||||
@@ -446,6 +446,19 @@ export function startHttpServer (
|
||||
false,
|
||||
false
|
||||
)
|
||||
} else if (s.specialError === 'migration') {
|
||||
cs.send(
|
||||
ctx,
|
||||
{
|
||||
id: -1,
|
||||
error: new Status(Severity.ERROR, platform.status.WorkspaceMigration, {
|
||||
workspaceUuid: token.workspace
|
||||
}),
|
||||
terminate: s.terminate
|
||||
},
|
||||
false,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
cs.send(
|
||||
ctx,
|
||||
|
||||
@@ -41,7 +41,15 @@ export class ApiEndpoint {
|
||||
return token
|
||||
}
|
||||
|
||||
async createWorkspaceWithLogin (workspaceName: string, username: string, password: string): Promise<any> {
|
||||
async createWorkspaceWithLogin (
|
||||
workspaceName: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<{
|
||||
workspace: string
|
||||
workspaceId: string
|
||||
workspaceName: string
|
||||
}> {
|
||||
const token = await this.loginAndGetToken(username, password)
|
||||
const url = this.baseUrl
|
||||
const payload = {
|
||||
@@ -50,7 +58,7 @@ export class ApiEndpoint {
|
||||
}
|
||||
const headers = this.getDefaultHeaders(token)
|
||||
const response = await this.request.post(url, { data: payload, headers })
|
||||
return await response.json()
|
||||
return (await response.json()).result
|
||||
}
|
||||
|
||||
async createAccount (username: string, password: string, firstName: string, lastName: string): Promise<any> {
|
||||
|
||||
@@ -8,6 +8,8 @@ export * from './model/select-workspace-page'
|
||||
export * from './utils'
|
||||
export * from './model/tracker/issues-page'
|
||||
export * from './model/tracker/issues-details-page'
|
||||
export * from './model/tracker/types'
|
||||
export * from './model/tracker/common-tracker-page'
|
||||
export * from './model/tracker/tracker-navigation-menu-page'
|
||||
export * from './model/profile/user-profile-page'
|
||||
export * from './API/Api'
|
||||
|
||||
@@ -31,6 +31,11 @@ export class LoginPage {
|
||||
await (await this.page.goto(`${PlatformURI}/login/login`))?.finished()
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
async gotoAdmin (): Promise<void> {
|
||||
await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished()
|
||||
}
|
||||
|
||||
async clickSignUp (): Promise<void> {
|
||||
await this.linkSignUp().click()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { test } from '@playwright/test'
|
||||
import { LoginPage } from '../model/login-page'
|
||||
import { DefaultWorkspace, generateId, PlatformURI, PlatformUser } from '../utils'
|
||||
import { SelectWorkspacePage } from '../model/select-workspace-page'
|
||||
@@ -10,8 +10,6 @@ import { IssuesPage } from '../model/tracker/issues-page'
|
||||
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
|
||||
import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page'
|
||||
import { SignInJoinPage } from '../model/signin-page'
|
||||
import { UserProfilePage } from '../model/profile/user-profile-page'
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
test.describe('Workspace tests', () => {
|
||||
let loginPage: LoginPage
|
||||
@@ -20,7 +18,6 @@ test.describe('Workspace tests', () => {
|
||||
let leftSideMenuPage: LeftSideMenuPage
|
||||
let trackerNavigationMenuPage: TrackerNavigationMenuPage
|
||||
let issuesPage: IssuesPage
|
||||
let userProfilePage: UserProfilePage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page)
|
||||
@@ -29,7 +26,6 @@ test.describe('Workspace tests', () => {
|
||||
leftSideMenuPage = new LeftSideMenuPage(page)
|
||||
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
|
||||
issuesPage = new IssuesPage(page)
|
||||
userProfilePage = new UserProfilePage(page)
|
||||
})
|
||||
|
||||
test('Create a workspace with a custom name', async () => {
|
||||
@@ -232,26 +228,4 @@ test.describe('Workspace tests', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('User can leave workspace', async () => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
await trackerNavigationMenuPage.checkIfTrackerSidebarIsVisible()
|
||||
await userProfilePage.openProfileMenu()
|
||||
await userProfilePage.selectProfileByName(newUser.lastName + ' ' + newUser.firstName)
|
||||
await userProfilePage.clickLeaveWorkspaceButton()
|
||||
await userProfilePage.clickLeaveWorkspaceCancelButton()
|
||||
await userProfilePage.clickLeaveWorkspaceButton()
|
||||
await userProfilePage.clickLeaveWorkspaceConfirmButton()
|
||||
await expect(selectWorkspacePage.title()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin"
|
||||
BACKUP_STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin"
|
||||
BACKUP_BUCKET_NAME=ws-dev-backups
|
||||
DB_URL=mongodb://mongodb:27018
|
||||
DB_EU_URL=postgresql://root@cockroach:26257/defaultdb?sslmode=disable
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"localhost:8083": {
|
||||
"key": "huly",
|
||||
"title": "Platform",
|
||||
"protocol": "http",
|
||||
"languages": "en,ru,pt,es,zh,fr",
|
||||
"defaultLanguage": "en",
|
||||
"defaultApplication": "tracker",
|
||||
"defaultSpace": "tracker:project:DefaultProject",
|
||||
"defaultSpecial": "issues",
|
||||
"lastNameFirst": "true",
|
||||
"links": [
|
||||
{
|
||||
"rel": "manifest",
|
||||
"href": "/platform/site.webmanifest"
|
||||
},
|
||||
{
|
||||
"rel": "icon",
|
||||
"href": "/platform/favicon.svg",
|
||||
"type": "image/svg+xml"
|
||||
},
|
||||
{
|
||||
"rel": "shortcut icon",
|
||||
"href": "/platform/favicon.ico",
|
||||
"sizes": "any"
|
||||
},
|
||||
{
|
||||
"rel": "apple-touch-icon",
|
||||
"href": "/platform/icon-192.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Restore workspace contents in mongo/elastic
|
||||
rush build
|
||||
rush bundle
|
||||
rush docker:build
|
||||
|
||||
# Re-assign user to workspace.
|
||||
docker compose -p sanity up $1 -d --force-recreate
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
./tool-local.sh create-workspace sanity-ws
|
||||
|
||||
./tool-local.sh create-account user1 -f John -l Appleseed -p 1234
|
||||
./tool-local.sh create-account user2 -f Kainin -l Dirak -p 1234
|
||||
@@ -0,0 +1,264 @@
|
||||
services:
|
||||
mongodb:
|
||||
image: 'mongo:7-jammy'
|
||||
command: mongod --port 27018
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
ports:
|
||||
- 27018:27018
|
||||
restart: unless-stopped
|
||||
cockroach:
|
||||
image: cockroachdb/cockroach:latest-v24.2
|
||||
ports:
|
||||
- '26258:26257'
|
||||
command: start-single-node --insecure
|
||||
restart: unless-stopped
|
||||
minio:
|
||||
image: 'minio/minio'
|
||||
command: server /data --address ":9000" --console-address ":9001"
|
||||
expose:
|
||||
- 9000
|
||||
- 9001
|
||||
ports:
|
||||
- 9002:9000
|
||||
- 9003:9001
|
||||
elastic:
|
||||
image: 'elasticsearch:7.14.2'
|
||||
expose:
|
||||
- 9200
|
||||
ports:
|
||||
- 9201:9200
|
||||
environment:
|
||||
- ELASTICSEARCH_PORT_NUMBER=9200
|
||||
- BITNAMI_DEBUG=true
|
||||
- discovery.type=single-node
|
||||
- ES_JAVA_OPTS=-Xms1024m -Xmx1024m
|
||||
healthcheck:
|
||||
interval: 20s
|
||||
retries: 10
|
||||
test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"'
|
||||
account:
|
||||
image: hardcoreeng/account
|
||||
pull_policy: never
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
- cockroach
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- REGION_INFO=|America;europe| # Europe without name will not be available for creation of new workspaces.
|
||||
# - REGION_INFO=|America;europe|Europe
|
||||
- ADMIN_EMAILS=admin
|
||||
- ACCOUNT_PORT=3003
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=mongodb://mongodb:27018
|
||||
- TRANSACTOR_URL=ws://transactor:3334;ws://localhost:3334,ws://transactor-europe:3335;ws://localhost:3335;europe,
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- MODEL_ENABLED=*
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://stats:4901
|
||||
workspace:
|
||||
image: hardcoreeng/workspace
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- WS_OPERATION=all+backup
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_URL}
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- MODEL_ENABLED=*
|
||||
- REGION=
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://stats:4901
|
||||
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
|
||||
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
|
||||
restart: unless-stopped
|
||||
workspace_europe:
|
||||
image: hardcoreeng/workspace
|
||||
links:
|
||||
- cockroach
|
||||
- minio
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- WS_OPERATION=all+backup
|
||||
- REGION=europe
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_EU_URL}
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- MODEL_ENABLED=*
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://stats:4901
|
||||
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
|
||||
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
|
||||
restart: unless-stopped
|
||||
front:
|
||||
image: hardcoreeng/front
|
||||
pull_policy: never
|
||||
links:
|
||||
- account
|
||||
- mongodb
|
||||
- minio
|
||||
- elastic
|
||||
- collaborator
|
||||
- transactor
|
||||
ports:
|
||||
- 8083:8083
|
||||
volumes:
|
||||
- ./branding-test.json:/app/dist/branding-test.json
|
||||
environment:
|
||||
- SERVER_PORT=8083
|
||||
- SERVER_SECRET=secret
|
||||
- ACCOUNTS_URL_INTERNAL=http://account:3003
|
||||
- ACCOUNTS_URL=http://localhost:3003
|
||||
- UPLOAD_URL=/files
|
||||
- ELASTIC_URL=http://elastic:9200
|
||||
- GMAIL_URL=http://localhost:8088
|
||||
- CALENDAR_URL=http://localhost:8095
|
||||
- REKONI_URL=http://rekoni:4007
|
||||
- TELEGRAM_URL=http://localhost:8086
|
||||
- COLLABORATOR_URL=ws://localhost:3079
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- BRANDING_URL=http://localhost:8083/branding-test.json
|
||||
- STATS_URL=http://stats:4901
|
||||
- STATS_API=http://localhost:4901
|
||||
transactor:
|
||||
image: hardcoreeng/transactor
|
||||
pull_policy: never
|
||||
links:
|
||||
- mongodb
|
||||
- elastic
|
||||
- minio
|
||||
- rekoni
|
||||
- account
|
||||
ports:
|
||||
- 3334:3334
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- REGION=
|
||||
- SERVER_PORT=3334
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_URL}
|
||||
- METRICS_CONSOLE=false
|
||||
- METRICS_FILE=metrics.txt
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- FRONT_URL=http://localhost:8083
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- COLLABORATOR_URL=http://collaborator:3079
|
||||
- LAST_NAME_FIRST=true
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- FULLTEXT_URL=http://fulltext:4710
|
||||
- STATS_URL=http://stats:4901
|
||||
- ENABLE_COMPRESSION=true
|
||||
transactor-europe:
|
||||
image: hardcoreeng/transactor
|
||||
pull_policy: never
|
||||
links:
|
||||
- elastic
|
||||
- minio
|
||||
- rekoni
|
||||
- cockroach
|
||||
- account
|
||||
ports:
|
||||
- 3335:3335
|
||||
volumes:
|
||||
- ./branding-test.json:/var/cfg/branding.json
|
||||
environment:
|
||||
- SERVER_PORT=3335
|
||||
- REGION=europe
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_EU_URL}
|
||||
- METRICS_CONSOLE=false
|
||||
- METRICS_FILE=metrics.txt
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- FRONT_URL=http://localhost:8083
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- COLLABORATOR_URL=http://collaborator:3079
|
||||
- LAST_NAME_FIRST=true
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- FULLTEXT_URL=http://fulltext:4710
|
||||
- STATS_URL=http://stats:4901
|
||||
- ENABLE_COMPRESSION=true
|
||||
collaborator:
|
||||
image: hardcoreeng/collaborator
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
- transactor
|
||||
ports:
|
||||
- 3079:3079
|
||||
environment:
|
||||
- COLLABORATOR_PORT=3079
|
||||
- SECRET=secret
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- FULLTEXT_URL=http://fulltext:4710
|
||||
- STATS_URL=http://stats:4901
|
||||
restart: unless-stopped
|
||||
rekoni:
|
||||
image: hardcoreeng/rekoni-service
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 4007:4004
|
||||
environment:
|
||||
- STATS_URL=http://stats:4901
|
||||
|
||||
fulltext:
|
||||
image: hardcoreeng/fulltext
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- elastic
|
||||
- mongodb
|
||||
- cockroach
|
||||
- rekoni
|
||||
ports:
|
||||
- 4710:4710
|
||||
environment:
|
||||
- PORT=4710
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_URL}
|
||||
- FULLTEXT_DB_URL=http://elastic:9200
|
||||
- ELASTIC_INDEX_NAME=local_storage_index
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- REKONI_URL=http://rekoni:4007
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- STATS_URL=http://stats:4901
|
||||
fulltext-europe:
|
||||
image: hardcoreeng/fulltext
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- elastic
|
||||
- mongodb
|
||||
- cockroach
|
||||
- rekoni
|
||||
ports:
|
||||
- 4711:4711
|
||||
environment:
|
||||
- PORT=4711
|
||||
- REGION=europe
|
||||
- SERVER_SECRET=secret
|
||||
- DB_URL=${DB_EU_URL}
|
||||
- FULLTEXT_DB_URL=http://elastic:9200
|
||||
- ELASTIC_INDEX_NAME=local_eu_storage_index # Use different one to be able to check migration is working
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- REKONI_URL=http://rekoni:4007
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- STATS_URL=http://stats:4901
|
||||
stats:
|
||||
image: hardcoreeng/stats
|
||||
ports:
|
||||
- 4901:4901
|
||||
environment:
|
||||
- PORT=4901
|
||||
- SERVER_SECRET=secret
|
||||
restart: unless-stopped
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker compose -p sanity kill
|
||||
docker compose -p sanity down --volumes
|
||||
docker compose -p sanity up -d --force-recreate --renew-anon-volumes
|
||||
docker_exit=$?
|
||||
if [ ${docker_exit} -eq 0 ]; then
|
||||
echo "Container started successfully"
|
||||
else
|
||||
echo "Container started with errors"
|
||||
exit ${docker_exit}
|
||||
fi
|
||||
|
||||
if [ "x$DO_CLEAN" == 'xtrue' ]; then
|
||||
echo 'Do docker Clean'
|
||||
docker system prune -a -f
|
||||
fi
|
||||
|
||||
./wait-elastic.sh 9201
|
||||
|
||||
# Create user record in accounts
|
||||
./tool.sh create-account admin -f Super -l Admin -p 1234
|
||||
./tool.sh create-account user1 -f John -l Appleseed -p 1234
|
||||
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
echo "Downloading profile"
|
||||
token=$(./tool.sh generate-token --admin anticrm@hc.engineering sanity-ws)
|
||||
current=$(date +%Y%m%d%H%M%S)
|
||||
mkdir -p ./profiles
|
||||
curl -X PUT "http://localhost:3334/api/v1/manage?token=${token}&operation=profile-stop" -o "./profiles/profile-${current}".cpuprofile
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# npm install -g cpupro
|
||||
for profile in $(ls ./profiles/*.cpuprofile); do
|
||||
name=${profile/\.cpuprofile/}
|
||||
if ! test -f $name.html; then
|
||||
cpupro $profile -f "$name.html" --no-open
|
||||
fi
|
||||
done
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
echo "Start profiling on server"
|
||||
token=$(./tool.sh generate-token --admin anticrm@hc.engineering sanity-ws)
|
||||
curl -X PUT "http://localhost:3334/api/v1/manage?token=${token}&operation=profile-start"
|
||||
@@ -0,0 +1,11 @@
|
||||
PLATFORM_URI='http://localhost:8083'
|
||||
PLATFORM_TRANSACTOR='ws://localhost:3334'
|
||||
STAGING_URL='https://front.hc.engineering'
|
||||
PLATFORM_USER='user1'
|
||||
PLATFORM_USER_SECOND='user2'
|
||||
PLATFORM_WS='sanity-ws'
|
||||
PLATFORM_TOKEN='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InVzZXIxIiwid29ya3NwYWNlIjoic2FuaXR5LXdzIn0.hfUCqePHO-WNps2by4B-CYGKIpDpLG0WVCUUtU-SVI4'
|
||||
LOCAL_URL='http://localhost:3003/'
|
||||
DEV_URL='http://localhost:8080/account'
|
||||
SETTING=.auth/storage.json
|
||||
SETTING_SECOND=.auth/storageSecond.json
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
playwright-report
|
||||
test-results/*
|
||||
allure-report
|
||||
allure-results
|
||||
.auth
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "default"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@hcengineering/tests-ws-sanity",
|
||||
"version": "0.6.1",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"template": "@hcengineering/default-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"_phase:build": "compile transpile tests",
|
||||
"_phase:test": "",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate",
|
||||
"lint:fix": "eslint --fix tests",
|
||||
"lint": "eslint tests",
|
||||
"format": "format tests",
|
||||
"ci": "playwright install --with-deps chromium",
|
||||
"test": "",
|
||||
"uitest": "cross-env LOCAL_URL=http://localhost:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts",
|
||||
"staging-uitest": "cross-env PLATFORM_URI=https://front.hc.engineering/ playwright test -c ./tests/playwright.config.ts --grep @staging",
|
||||
"dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 DEV_URL=http://localhost:8080/account playwright test -c ./tests/playwright.config.ts",
|
||||
"debug": "cross-env LOCAL_URL=http://localhost:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts --debug --headed",
|
||||
"dev-debug": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 playwright test -c ./tests/playwright.config.ts --debug --headed",
|
||||
"codegen": "playwright codegen --load-storage .auth/storage.json http://localhost:8083/login/selectWorkspace",
|
||||
"codegen-admin": "playwright codegen --load-storage .auth/adminStorage.json http://localhost:8083/login/admin",
|
||||
"dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench/sanity-ws/",
|
||||
"allure:generate": "allure generate allure-results -o allure-report --clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "~20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"allure-playwright": "^3.0.6",
|
||||
"allure-js-commons": "^3.0.6",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hcengineering/tests-sanity": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "~16.0.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/client-resources": "^0.6.27"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Page, test as setup } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import { LoginPage, PlatformURI, PlatformUser, PlatformUserSecond, setTestOptions } from '@hcengineering/tests-sanity'
|
||||
import { existsSync } from 'fs'
|
||||
|
||||
const authFile = path.join(__dirname, '../../.auth/storage.json')
|
||||
const authFileSecond = path.join(__dirname, '../../.auth/storageSecond.json')
|
||||
const adminFile = path.join(__dirname, '../../.auth/adminStorage.json')
|
||||
|
||||
async function authenticate (page: Page, user: string, password: string): Promise<void> {
|
||||
const loginPage = new LoginPage(page)
|
||||
await (await page.goto(`${PlatformURI}`))?.finished()
|
||||
await loginPage.login(user, password)
|
||||
}
|
||||
|
||||
if (!existsSync(authFile)) {
|
||||
setup('auth user1', async ({ page }) => {
|
||||
await authenticate(page, PlatformUser, '1234')
|
||||
await setTestOptions(page)
|
||||
|
||||
await page.context().storageState({ path: authFile })
|
||||
})
|
||||
}
|
||||
|
||||
if (!existsSync(authFileSecond)) {
|
||||
setup('auth user2', async ({ page }) => {
|
||||
await authenticate(page, PlatformUserSecond, '1234')
|
||||
await setTestOptions(page)
|
||||
|
||||
await page.context().storageState({ path: authFileSecond })
|
||||
})
|
||||
}
|
||||
|
||||
if (!existsSync(adminFile)) {
|
||||
setup('auth admin', async ({ page }) => {
|
||||
await authenticate(page, 'admin', '1234')
|
||||
await setTestOptions(page)
|
||||
|
||||
await page.context().storageState({ path: adminFile })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PlatformURI } from '@hcengineering/tests-sanity'
|
||||
import { type Page } from '@playwright/test'
|
||||
|
||||
export class AdminPage {
|
||||
readonly page: Page
|
||||
|
||||
constructor (page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
async gotoAdmin (): Promise<void> {
|
||||
await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { devices, PlaywrightTestConfig } from '@playwright/test'
|
||||
import { config as dotenvConfig } from 'dotenv'
|
||||
dotenvConfig()
|
||||
|
||||
let maxFailures: number | undefined
|
||||
if (process.env.TESTS_MAX_FAILURES !== undefined) {
|
||||
maxFailures = parseInt(process.env.TESTS_MAX_FAILURES)
|
||||
}
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: 'Platform',
|
||||
use: {
|
||||
testIdAttribute: 'data-id',
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
...devices['Desktop Chrome'],
|
||||
screenshot: 'only-on-failure',
|
||||
viewport: {
|
||||
width: 1440,
|
||||
height: 900
|
||||
},
|
||||
trace: {
|
||||
mode: 'retain-on-failure',
|
||||
snapshots: true,
|
||||
screenshots: true,
|
||||
sources: true
|
||||
},
|
||||
contextOptions: {
|
||||
reducedMotion: 'reduce'
|
||||
}
|
||||
},
|
||||
fullyParallel: false,
|
||||
dependencies: ['setup']
|
||||
}
|
||||
],
|
||||
retries: 2,
|
||||
timeout: 60000,
|
||||
maxFailures,
|
||||
expect: {
|
||||
timeout: 15000
|
||||
},
|
||||
reporter: [
|
||||
['list'],
|
||||
['html'],
|
||||
[
|
||||
'allure-playwright',
|
||||
{
|
||||
detail: true,
|
||||
suiteTitle: false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ApiEndpoint,
|
||||
generateId,
|
||||
getSecondPage,
|
||||
IssuesDetailsPage,
|
||||
IssuesPage,
|
||||
LoginPage,
|
||||
NewIssue,
|
||||
SelectWorkspacePage,
|
||||
TrackerNavigationMenuPage
|
||||
} from '@hcengineering/tests-sanity'
|
||||
import { test } from '@playwright/test'
|
||||
import { AdminPage } from '../model/admin.page'
|
||||
|
||||
test.describe('Workspace Archive tests', () => {
|
||||
let loginPage: LoginPage
|
||||
let selectWorkspacePage: SelectWorkspacePage
|
||||
let trackerNavigationMenuPage: TrackerNavigationMenuPage
|
||||
let issuesPage: IssuesPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page)
|
||||
selectWorkspacePage = new SelectWorkspacePage(page)
|
||||
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
|
||||
issuesPage = new IssuesPage(page)
|
||||
})
|
||||
|
||||
test('New workspace with date, archive, unarchive', async ({ page, browser, request }) => {
|
||||
const api: ApiEndpoint = new ApiEndpoint(request)
|
||||
const workspaceName = generateId()
|
||||
const workspaceInfo = await api.createWorkspaceWithLogin(workspaceName, 'user1', '1234')
|
||||
|
||||
const newIssue: NewIssue = {
|
||||
title: `Issue with all parameters and attachments-${generateId()}`,
|
||||
description: 'Created issue with all parameters and attachments description',
|
||||
status: 'In Progress',
|
||||
priority: 'Urgent',
|
||||
createLabel: true,
|
||||
labels: `CREATE-ISSUE-${generateId()}`,
|
||||
component: 'No component',
|
||||
estimation: '2',
|
||||
milestone: 'No Milestone',
|
||||
duedate: 'today'
|
||||
}
|
||||
await test.step('create new workspace', async () => {
|
||||
await loginPage.goto()
|
||||
await loginPage.login('user1', '1234')
|
||||
|
||||
await selectWorkspacePage.selectWorkspace(workspaceName)
|
||||
|
||||
await trackerNavigationMenuPage.openIssuesForProject('Default')
|
||||
await issuesPage.clickModelSelectorAll()
|
||||
await issuesPage.createNewIssue(newIssue)
|
||||
await issuesPage.openIssueByName(newIssue.title)
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
})
|
||||
|
||||
using adminSecondPage = await getSecondPage(browser)
|
||||
const page2 = adminSecondPage.page
|
||||
|
||||
await test.step('Archive workspace', async () => {
|
||||
// login as admin
|
||||
const loginPage2 = new LoginPage(adminSecondPage.page)
|
||||
await loginPage2.goto()
|
||||
await loginPage2.login('admin', '1234')
|
||||
|
||||
const adminPage = new AdminPage(page2)
|
||||
await adminPage.gotoAdmin()
|
||||
|
||||
await page2.getByText('Today -').click()
|
||||
await page2.locator('div:nth-child(3) > .checkbox-container > .checkSVG').click()
|
||||
await page2.locator('div:nth-child(4) > .checkbox-container > .checkSVG').click()
|
||||
|
||||
await page2.getByRole('button', { name: 'America', exact: true }).click()
|
||||
await page2.getByRole('button', { name: 'europe (hidden)' }).click()
|
||||
await page2.getByPlaceholder('Search').click()
|
||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspace)
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByRole('button', { name: 'Archive' }).click()
|
||||
|
||||
await page2.getByRole('button', { name: 'Ok' }).click()
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByText('archived').waitFor()
|
||||
})
|
||||
await test.step('Check workspace is archived', async () => {
|
||||
await page.reload()
|
||||
await page.getByText('Workspace is archived').waitFor()
|
||||
})
|
||||
await test.step('Restore workspace', async () => {
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByRole('button', { name: 'Unarchive' }).click()
|
||||
|
||||
await page2.getByRole('button', { name: 'Ok' }).click()
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByText('active').waitFor()
|
||||
})
|
||||
await test.step('Check workspace is active again', async () => {
|
||||
await page.reload()
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,228 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import {
|
||||
generateId,
|
||||
IssuesDetailsPage,
|
||||
IssuesPage,
|
||||
LeftSideMenuPage,
|
||||
LoginPage,
|
||||
NewIssue,
|
||||
SelectWorkspacePage,
|
||||
SignInJoinPage,
|
||||
SignUpData,
|
||||
SignUpPage,
|
||||
TrackerNavigationMenuPage,
|
||||
UserProfilePage
|
||||
} from '@hcengineering/tests-sanity'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Workspace tests', () => {
|
||||
let loginPage: LoginPage
|
||||
let signUpPage: SignUpPage
|
||||
let selectWorkspacePage: SelectWorkspacePage
|
||||
let leftSideMenuPage: LeftSideMenuPage
|
||||
let trackerNavigationMenuPage: TrackerNavigationMenuPage
|
||||
let issuesPage: IssuesPage
|
||||
let userProfilePage: UserProfilePage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page)
|
||||
signUpPage = new SignUpPage(page)
|
||||
selectWorkspacePage = new SelectWorkspacePage(page)
|
||||
leftSideMenuPage = new LeftSideMenuPage(page)
|
||||
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
|
||||
issuesPage = new IssuesPage(page)
|
||||
userProfilePage = new UserProfilePage(page)
|
||||
})
|
||||
|
||||
test('Create a workspace with a custom name', async () => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: `FirstName-${generateId()}`,
|
||||
lastName: `LastName-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
await leftSideMenuPage.clickTracker()
|
||||
})
|
||||
|
||||
test('Create a new issue in the workspace with a custom name', async ({ page }) => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: `FirstName-${generateId()}`,
|
||||
lastName: `LastName-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
const newIssue: NewIssue = {
|
||||
title: `Issue with all parameters and attachments-${generateId()}`,
|
||||
description: 'Created issue with all parameters and attachments description',
|
||||
status: 'In Progress',
|
||||
priority: 'Urgent',
|
||||
assignee: `${newUser.lastName} ${newUser.firstName}`,
|
||||
createLabel: true,
|
||||
labels: `CREATE-ISSUE-${generateId()}`,
|
||||
component: 'No component',
|
||||
estimation: '2',
|
||||
milestone: 'No Milestone',
|
||||
duedate: 'today'
|
||||
}
|
||||
const newWorkspaceName = `New Issue Name - ${generateId(2)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
|
||||
await trackerNavigationMenuPage.openIssuesForProject('Default')
|
||||
await issuesPage.clickModelSelectorAll()
|
||||
await issuesPage.createNewIssue(newIssue)
|
||||
await issuesPage.openIssueByName(newIssue.title)
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
})
|
||||
|
||||
test('Check validation steps description for the create flow', async ({ page }) => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: `FirstName-${generateId()}`,
|
||||
lastName: `LastName-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
|
||||
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
|
||||
await signUpPage.checkInfo(page, 'Required field First name')
|
||||
await signUpPage.enterFirstName(newUser.firstName)
|
||||
await signUpPage.checkInfo(page, 'Required field Last name')
|
||||
await signUpPage.enterLastName(newUser.lastName)
|
||||
await signUpPage.checkInfo(page, 'Required field Email')
|
||||
await signUpPage.enterEmail(newUser.email)
|
||||
await signUpPage.checkInfo(page, 'Required field Password')
|
||||
await signUpPage.enterPassword(newUser.password)
|
||||
await signUpPage.checkInfo(page, "Repeat password don't match Password")
|
||||
await signUpPage.enterRepeatPassword(newUser.password)
|
||||
await signUpPage.checkInfoSectionNotExist(page)
|
||||
await signUpPage.clickSignUp()
|
||||
await selectWorkspacePage.checkInfo(page, 'Required field Workspace name')
|
||||
await selectWorkspacePage.enterWorkspaceName(newWorkspaceName)
|
||||
await selectWorkspacePage.checkInfoSectionNotExist(page)
|
||||
})
|
||||
|
||||
test('Create a workspace with join link', async ({ page, browser }) => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: `FirstName-${generateId()}`,
|
||||
lastName: `LastName-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
await leftSideMenuPage.clickTracker()
|
||||
|
||||
// Generate invite link
|
||||
|
||||
await leftSideMenuPage.openProfileMenu()
|
||||
await leftSideMenuPage.inviteToWorkspace()
|
||||
await leftSideMenuPage.getInviteLink()
|
||||
|
||||
const linkText = await page.locator('.antiPopup .link').textContent()
|
||||
const page2 = await browser.newPage()
|
||||
try {
|
||||
await page2.goto(linkText ?? '')
|
||||
const newUser2: SignUpData = {
|
||||
firstName: `FirstName2-${generateId()}`,
|
||||
lastName: `LastName2-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
|
||||
await page2.getByRole('link', { name: 'Sign Up' }).click()
|
||||
const signUpPage2 = new SignUpPage(page2)
|
||||
await signUpPage2.signUp(newUser2, 'join')
|
||||
|
||||
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
|
||||
await leftSideMenuPage2.clickTracker()
|
||||
} finally {
|
||||
await page2.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('Create a workspace with join link - existing account', async ({ page, browser }) => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: `FirstName-${generateId()}`,
|
||||
lastName: `LastName-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
await leftSideMenuPage.clickTracker()
|
||||
|
||||
// Generate invite link
|
||||
await leftSideMenuPage.openProfileMenu()
|
||||
await leftSideMenuPage.inviteToWorkspace()
|
||||
await leftSideMenuPage.getInviteLink()
|
||||
|
||||
const linkText = await page.locator('.antiPopup .link').textContent()
|
||||
const page2 = await browser.newPage()
|
||||
try {
|
||||
const loginPage2 = new LoginPage(page2)
|
||||
await loginPage2.goto()
|
||||
await loginPage2.clickSignUp()
|
||||
|
||||
const newUser2: SignUpData = {
|
||||
firstName: `FirstName2-${generateId()}`,
|
||||
lastName: `LastName2-${generateId()}`,
|
||||
email: `sanity-email+${generateId()}@gmail.com`,
|
||||
password: '1234'
|
||||
}
|
||||
|
||||
const signUpPage2 = new SignUpPage(page2)
|
||||
await signUpPage2.signUp(newUser2)
|
||||
|
||||
// Ok we signed in, and no workspace present.
|
||||
await page2.goto(linkText ?? '')
|
||||
const joinPage = new SignInJoinPage(page2)
|
||||
await joinPage.join(newUser2)
|
||||
|
||||
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
|
||||
await leftSideMenuPage2.clickTracker()
|
||||
} finally {
|
||||
await page2.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('User can leave workspace', async ({ page }) => {
|
||||
const newUser: SignUpData = {
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
password: '1234'
|
||||
}
|
||||
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
|
||||
await loginPage.goto()
|
||||
await loginPage.clickSignUp()
|
||||
await signUpPage.signUp(newUser)
|
||||
await selectWorkspacePage.createWorkspace(newWorkspaceName)
|
||||
await trackerNavigationMenuPage.checkIfTrackerSidebarIsVisible()
|
||||
await userProfilePage.openProfileMenu()
|
||||
await userProfilePage.selectProfileByName(newUser.lastName + ' ' + newUser.firstName)
|
||||
await userProfilePage.clickLeaveWorkspaceButton()
|
||||
await userProfilePage.clickLeaveWorkspaceCancelButton()
|
||||
await userProfilePage.clickLeaveWorkspaceButton()
|
||||
await userProfilePage.clickLeaveWorkspaceConfirmButton()
|
||||
await expect(page.locator('form')).toContainText('Select workspace')
|
||||
await expect(page.getByText(newWorkspaceName)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ApiEndpoint,
|
||||
generateId,
|
||||
getSecondPage,
|
||||
IssuesDetailsPage,
|
||||
IssuesPage,
|
||||
LoginPage,
|
||||
NewIssue,
|
||||
SelectWorkspacePage,
|
||||
TrackerNavigationMenuPage
|
||||
} from '@hcengineering/tests-sanity'
|
||||
import { test } from '@playwright/test'
|
||||
import { AdminPage } from '../model/admin.page'
|
||||
|
||||
test.describe('Workspace Migration tests', () => {
|
||||
let loginPage: LoginPage
|
||||
let selectWorkspacePage: SelectWorkspacePage
|
||||
let trackerNavigationMenuPage: TrackerNavigationMenuPage
|
||||
let issuesPage: IssuesPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page)
|
||||
selectWorkspacePage = new SelectWorkspacePage(page)
|
||||
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
|
||||
issuesPage = new IssuesPage(page)
|
||||
})
|
||||
|
||||
test('New workspace migrate to europe', async ({ page, browser, request }) => {
|
||||
const api: ApiEndpoint = new ApiEndpoint(request)
|
||||
const workspaceName = generateId()
|
||||
const workspaceInfo = await api.createWorkspaceWithLogin(workspaceName, 'user1', '1234')
|
||||
|
||||
const newIssue: NewIssue = {
|
||||
title: `Issue with all parameters and attachments-${generateId()}`,
|
||||
description: 'Created issue with all parameters and attachments description',
|
||||
status: 'In Progress',
|
||||
priority: 'Urgent',
|
||||
createLabel: true,
|
||||
labels: `CREATE-ISSUE-${generateId()}`,
|
||||
component: 'No component',
|
||||
estimation: '2',
|
||||
milestone: 'No Milestone',
|
||||
duedate: 'today'
|
||||
}
|
||||
await test.step('create new workspace', async () => {
|
||||
await loginPage.goto()
|
||||
await loginPage.login('user1', '1234')
|
||||
|
||||
await selectWorkspacePage.selectWorkspace(workspaceName)
|
||||
|
||||
await trackerNavigationMenuPage.openIssuesForProject('Default')
|
||||
await issuesPage.clickModelSelectorAll()
|
||||
await issuesPage.createNewIssue(newIssue)
|
||||
await issuesPage.openIssueByName(newIssue.title)
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
})
|
||||
|
||||
using adminSecondPage = await getSecondPage(browser)
|
||||
const page2 = adminSecondPage.page
|
||||
|
||||
await test.step('Migrate workspace', async () => {
|
||||
// login as admin
|
||||
const loginPage2 = new LoginPage(adminSecondPage.page)
|
||||
await loginPage2.goto()
|
||||
await loginPage2.login('admin', '1234')
|
||||
|
||||
const adminPage = new AdminPage(page2)
|
||||
await adminPage.gotoAdmin()
|
||||
|
||||
await page2.getByText('Today -').click()
|
||||
await page2.locator('div:nth-child(3) > .checkbox-container > .checkSVG').click()
|
||||
await page2.locator('div:nth-child(4) > .checkbox-container > .checkSVG').click()
|
||||
|
||||
await page2.getByRole('button', { name: 'America', exact: true }).click()
|
||||
await page2.getByRole('button', { name: 'europe (hidden)' }).click()
|
||||
await page2.getByPlaceholder('Search').click()
|
||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspace)
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByRole('button', { name: 'Migrate' }).click()
|
||||
|
||||
await page2.getByRole('button', { name: 'Ok' }).click()
|
||||
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByText('europe').waitFor()
|
||||
})
|
||||
await test.step('Check workspace is active again', async () => {
|
||||
await page.reload()
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./tests",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"lib": ["esnext", "dom"],
|
||||
}
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export MODEL_VERSION=$(node ../common/scripts/show_version.js)
|
||||
export MINIO_ACCESS_KEY=minioadmin
|
||||
export MINIO_SECRET_KEY=minioadmin
|
||||
export MINIO_ENDPOINT=localhost:9002
|
||||
export ACCOUNTS_URL=http://localhost:3003
|
||||
export TRANSACTOR_URL=ws://localhost:3334
|
||||
# export ACCOUNT_DB_URL=postgresql://root@localhost:26258/defaultdb?sslmode=disable
|
||||
export ACCOUNT_DB_URL=mongodb://localhost:27018
|
||||
export MONGO_URL=mongodb://localhost:27018
|
||||
export ELASTIC_URL=http://localhost:9201
|
||||
export SERVER_SECRET=secret
|
||||
export DB_URL=postgresql://root@localhost:26258/defaultdb?sslmode=disable
|
||||
|
||||
node ${TOOL_OPTIONS} ../dev/tool/bundle/bundle.js $@
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
res=''
|
||||
port=$1
|
||||
echo "Warning Elastic to up and running with attachment processor... ${port}"
|
||||
for i in `seq 1 30`;
|
||||
do
|
||||
res=$(curl -s http://localhost:${port}/_cluster/health )
|
||||
echo "$res"
|
||||
if [[ $res = *"yellow"* ]]; then
|
||||
echo "Elastic up and running..."
|
||||
exit 0
|
||||
fi
|
||||
if [[ $res = *"green"* ]]; then
|
||||
echo "Elastic up and running..."
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
Reference in New Issue
Block a user