Merge staging 12feb (#7993)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-12 23:18:50 +07:00
committed by GitHub
parent 1a76ebcc80
commit db0bbe31aa
71 changed files with 1607 additions and 357 deletions
+90 -51
View File
@@ -40,6 +40,8 @@ env:
rush.json rush.json
.prettierrc .prettierrc
tools tools
workers
ws-tests
PublishTempFolder: publish_artifacts PublishTempFolder: publish_artifacts
INIT_SCRIPTS_BRANCH: 'unified-init-scripts' INIT_SCRIPTS_BRANCH: 'unified-init-scripts'
@@ -81,13 +83,13 @@ jobs:
run: node common/scripts/install-run-rush.js model-version run: node common/scripts/install-run-rush.js model-version
- name: Building... - name: Building...
run: node common/scripts/install-run-rush.js build -p 20 run: node common/scripts/install-run-rush.js build
- name: Bundle... - name: Bundle...
run: node common/scripts/install-run-rush.js bundle -p 20 run: node common/scripts/install-run-rush.js bundle
- name: Validate... - 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 - name: Cache build results
uses: actions/cache@v4 uses: actions/cache@v4
@@ -154,7 +156,7 @@ jobs:
[ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ] [ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ]
echo '================================================================' echo '================================================================'
- name: Formatting... - 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 - name: Check files formatting
run: | run: |
echo '================================================================' echo '================================================================'
@@ -223,7 +225,7 @@ jobs:
run: node common/scripts/install-run-rush.js install run: node common/scripts/install-run-rush.js install
- name: Docker Build - 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: env:
DOCKER_CLI_HINTS: false DOCKER_CLI_HINTS: false
- name: Prepare server - name: Prepare server
@@ -260,10 +262,8 @@ jobs:
run: | run: |
cd ./tests/sanity cd ./tests/sanity
mkdir logs mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log cd logs
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
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
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -340,7 +340,7 @@ jobs:
run: node common/scripts/install-run-rush.js install run: node common/scripts/install-run-rush.js install
- name: Docker Build - 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: env:
DOCKER_CLI_HINTS: false DOCKER_CLI_HINTS: false
- name: Prepare server - name: Prepare server
@@ -361,9 +361,8 @@ jobs:
run: | run: |
cd ./tests/sanity cd ./tests/sanity
mkdir logs mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log cd logs
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -405,12 +404,12 @@ jobs:
run: node common/scripts/install-run-rush.js install run: node common/scripts/install-run-rush.js install
- name: Docker Build - 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: env:
DOCKER_CLI_HINTS: false DOCKER_CLI_HINTS: false
- name: Configure /etc/hosts - name: Configure /etc/hosts
run: | 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 - name: Prepare server
run: | run: |
cd ./qms-tests cd ./qms-tests
@@ -429,50 +428,90 @@ jobs:
run: | run: |
cd ./qms-tests/sanity cd ./qms-tests/sanity
mkdir logs mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log cd logs
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: playwright-results-qms name: playwright-results-qms
path: ./qms-tests/sanity/playwright-report/ 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 - name: Upload Logs
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: docker-logs-qms name: docker-logs-qms
path: ./qms-tests/sanity/logs 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: docker-build:
needs: [build, test, svelte-check, uitest] needs: [build, test, svelte-check, uitest]
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -531,9 +570,9 @@ jobs:
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
- name: Docker build love-agent - name: Docker build love-agent
run: | run: |
cd ./services/ai-bot/love-agent cd ./services/ai-bot/love-agent
pnpm install && pnpm build pnpm install && pnpm build
pnpm docker:build -v pnpm docker:build -v
env: env:
DOCKER_CLI_HINTS: false DOCKER_CLI_HINTS: false
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
@@ -590,7 +629,7 @@ jobs:
- name: Model version from git tags - name: Model version from git tags
run: node common/scripts/install-run-rush.js model-version run: node common/scripts/install-run-rush.js model-version
- name: Package - 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 - name: Package JSON
run: | run: |
cd desktop-package cd desktop-package
+1 -1
View File
@@ -9,7 +9,7 @@ temp/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
tests/sanity/screenshots *tests/sanity/screenshots
# Runtime data # Runtime data
*.pid *.pid
+65
View File
@@ -70,6 +70,38 @@
"cwd": "${workspaceRoot}/pods/server", "cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector" "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", "name": "Debug Fulltext",
"type": "node", "type": "node",
@@ -132,6 +164,39 @@
"cwd": "${workspaceRoot}/pods/account", "cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector" "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", "name": "Debug Stats",
"type": "node", "type": "node",
+11 -1
View File
@@ -331,7 +331,7 @@
"summary": "Format changed projects", "summary": "Format changed projects",
"description": "Format and autofix linting issues in changed projects", "description": "Format and autofix linting issues in changed projects",
"safeForSimultaneousRushProcesses": true, "safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/each-diff.sh rushx format --force" "shellCommand": "./common/scripts/fast-format.sh"
}, },
{ {
"commandKind": "global", "commandKind": "global",
@@ -367,6 +367,16 @@
"shortName": "-f", "shortName": "-f",
"description": "Force formatting", "description": "Force formatting",
"associatedCommands": ["format"] "associatedCommands": ["format"]
},
{
"parameterKind": "string",
"argumentName": "BRANCH",
"required": false,
"associatedPhases": [],
"shortName": "-b",
"longName": "--branch",
"description": "Force formatting of branch",
"associatedCommands": ["fast-format"]
} }
// { // {
// /** // /**
+29
View File
@@ -1087,6 +1087,9 @@ importers:
'@rush-temp/tests-sanity': '@rush-temp/tests-sanity':
specifier: file:./projects/tests-sanity.tgz specifier: file:./projects/tests-sanity.tgz
version: 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': '@rush-temp/text':
specifier: file:./projects/text.tgz 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) 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} resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
version: 0.0.0 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': '@rush-temp/text-core@file:projects/text-core.tgz':
resolution: {integrity: sha512-sQ1C/LTDJ6RoC7YHYOCrRGsOHerkeumxp9lRjRaTmjc65mAFtiizz4eYFlkbSVSQvahSdD5KMT1G+wvtmLq7Gg==, tarball: file:projects/text-core.tgz} resolution: {integrity: sha512-sQ1C/LTDJ6RoC7YHYOCrRGsOHerkeumxp9lRjRaTmjc65mAFtiizz4eYFlkbSVSQvahSdD5KMT1G+wvtmLq7Gg==, tarball: file:projects/text-core.tgz}
version: 0.0.0 version: 0.0.0
@@ -24910,6 +24917,28 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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)': '@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: dependencies:
'@types/jest': 29.5.12 '@types/jest': 29.5.12
+3 -1
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/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 [ -z "$FILES" ] && exit 0
roots=$(rush list -p --json | grep "path" | cut -f 2 -d ':' | cut -f 2 -d '"') roots=$(rush list -p --json | grep "path" | cut -f 2 -d ':' | cut -f 2 -d '"')
+12
View File
@@ -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
-2
View File
@@ -41,8 +41,6 @@ async function doBackup (dirName: string, token: string, endpoint: string, wsIds
ctx.info('do backup', { ...wsIds, endpoint }) ctx.info('do backup', { ...wsIds, endpoint })
await backup(ctx, endpoint, wsIds, storage, { await backup(ctx, endpoint, wsIds, storage, {
force: true, force: true,
freshBackup: false,
clean: false,
skipDomains: [], skipDomains: [],
timeout: 0, timeout: 0,
connectTimeout: 60 * 1000, connectTimeout: 60 * 1000,
+1
View File
@@ -10,6 +10,7 @@
"analyze": "rm -rf ./dist && cross-env NODE_ENV=production webpack --profile --json > stats.json", "analyze": "rm -rf ./dist && cross-env NODE_ENV=production webpack --profile --json > stats.json",
"show": "webpack-bundle-analyzer stats.json dist", "show": "webpack-bundle-analyzer stats.json dist",
"dev-server": "cross-env USE_CACHE=false CLIENT_TYPE=dev-server webpack serve", "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": "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-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", "dev-server-cache": "cross-env USE_CACHE=true CLIENT_TYPE=dev-server webpack serve",
+22
View File
@@ -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"
}
+1
View File
@@ -196,6 +196,7 @@ const configs: Record<string, string> = {
'dev-huly': '/config-huly.json', 'dev-huly': '/config-huly.json',
'dev-bold': '/config.json', 'dev-bold': '/config.json',
'dev-server': '/config.json', 'dev-server': '/config.json',
'dev-server-test': '/config-test.json',
'dev-worker': '/config-worker.json', 'dev-worker': '/config-worker.json',
'dev-worker-local': '/config-worker-local.json' 'dev-worker-local': '/config-worker-local.json'
} }
+33 -1
View File
@@ -24,13 +24,14 @@ const mode = process.env.NODE_ENV || 'development'
const prod = mode === 'production' const prod = mode === 'production'
const clientType = process.env.CLIENT_TYPE ?? '' const clientType = process.env.CLIENT_TYPE ?? ''
const devServer = clientType === 'dev-server' const devServer = clientType === 'dev-server'
const devServerTest = clientType === 'dev-server-test'
const devServerWorker = clientType === 'dev-worker' const devServerWorker = clientType === 'dev-worker'
const devServerWorkerLocal = clientType === 'dev-worker-local' const devServerWorkerLocal = clientType === 'dev-worker-local'
const devProduction = clientType === 'dev-production' const devProduction = clientType === 'dev-production'
const devProductionHuly = clientType === 'dev-huly' const devProductionHuly = clientType === 'dev-huly'
const devProductionBold = clientType === 'dev-bold' const devProductionBold = clientType === 'dev-bold'
const dev = 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 ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const { EsbuildPlugin } = require('esbuild-loader') 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 = { const devHulyProxy = {
'/account': { '/account': {
target: 'https://account.huly.app/', target: 'https://account.huly.app/',
@@ -148,6 +179,7 @@ const proxy = {
'dev-worker': devProxy, 'dev-worker': devProxy,
'dev-worker-local': devProxy, 'dev-worker-local': devProxy,
'dev-server': devProxy, 'dev-server': devProxy,
'dev-server-test': devProxyTest,
'dev-production': devFrontProxy, 'dev-production': devFrontProxy,
'dev-bold': devBoldProxy, 'dev-bold': devBoldProxy,
'dev-huly': devHulyProxy 'dev-huly': devHulyProxy
+28
View File
@@ -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
-10
View File
@@ -589,8 +589,6 @@ export function devTool (
// }) // })
// }, // },
// cmd.region, // cmd.region,
// true,
// true,
// 5000, // 5 gigabytes per blob // 5000, // 5 gigabytes per blob
// sharedPipelineContextVars, // sharedPipelineContextVars,
// async (storage, workspaceStorage) => { // async (storage, workspaceStorage) => {
@@ -661,8 +659,6 @@ export function devTool (
// }) // })
// }, // },
// cmd.region, // cmd.region,
// false,
// false,
// 100, // 100,
// sharedPipelineContextVars // sharedPipelineContextVars
// ) // )
@@ -859,8 +855,6 @@ export function devTool (
// ) // )
// .option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 15mb)', '15') // .option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 15mb)', '15')
// .option('-f, --force', 'Force backup', false) // .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') // .option('-t, --timeout <timeout>', 'Connect timeout in seconds', '30')
// .action( // .action(
// async ( // async (
@@ -869,8 +863,6 @@ export function devTool (
// cmd: { // cmd: {
// skip: string // skip: string
// force: boolean // force: boolean
// fresh: boolean
// clean: boolean
// timeout: string // timeout: string
// include: string // include: string
// blobLimit: string // blobLimit: string
@@ -882,8 +874,6 @@ export function devTool (
// const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external') // const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external')
// await backup(toolCtx, endpoint, wsIds, storage, { // await backup(toolCtx, endpoint, wsIds, storage, {
// force: cmd.force, // force: cmd.force,
// freshBackup: cmd.fresh,
// clean: cmd.clean,
// include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())), // include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())),
// skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()), // skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()),
// timeout: 0, // timeout: 0,
+1
View File
@@ -150,6 +150,7 @@ export default plugin(platformId, {
AccountNotConfirmed: '' as StatusCode, AccountNotConfirmed: '' as StatusCode,
WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>, WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>,
WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>, WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>,
WorkspaceMigration: '' as StatusCode<{ workspaceUuid: string }>,
SocialIdNotFound: '' as StatusCode<{ socialId: string, type: string }>, SocialIdNotFound: '' as StatusCode<{ socialId: string, type: string }>,
SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>, SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>,
SocialIdAlreadyConfirmed: '' 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) { if (resp.error?.code === platform.status.WorkspaceArchived) {
this.opt?.onArchived?.() this.opt?.onArchived?.()
} }
if (resp.error?.code === platform.status.WorkspaceMigration) {
this.opt?.onMigration?.()
}
} }
if (resp.id !== undefined) { if (resp.id !== undefined) {
+1
View File
@@ -63,6 +63,7 @@ export interface ClientFactoryOptions {
onUpgrade?: () => void onUpgrade?: () => void
onUnauthorized?: () => void onUnauthorized?: () => void
onArchived?: () => void onArchived?: () => void
onMigration?: () => void
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>
ctx?: MeasureContext ctx?: MeasureContext
onDialTimeout?: () => void | Promise<void> onDialTimeout?: () => void | Promise<void>
@@ -27,11 +27,13 @@
Scroller, Scroller,
SearchEdit, SearchEdit,
showPopup, showPopup,
ticker ticker,
Grid
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation } from '../utils' import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation } from '../utils'
import { RegionInfo } from '@hcengineering/account-client' import { RegionInfo } from '@hcengineering/account-client'
import ColStar from '@hcengineering/ui/src/components/icons/ColStar.svelte'
$: now = $ticker $: now = $ticker
@@ -44,7 +46,7 @@
window.open(url, '_blank') window.open(url, '_blank')
} }
type WorkspaceInfo = WorkspaceInfoWithStatus & { attempts: number } type WorkspaceInfo = WorkspaceInfoWithStatus & { processingAttempts: number }
let workspaces: WorkspaceInfo[] = [] let workspaces: WorkspaceInfo[] = []
@@ -193,13 +195,19 @@
let selectedRegionId: string = '' let selectedRegionId: string = ''
void getRegionInfo().then((_regionInfo) => { void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo?.filter((it) => it.name !== '') ?? [] regionInfo = _regionInfo ?? []
if (selectedRegionId === '' && regionInfo.length > 0) { if (selectedRegionId === '' && regionInfo.length > 0) {
selectedRegionId = regionInfo[0].region 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( $: byVersion = groupByArray(
workspaces.filter((it) => { workspaces.filter((it) => {
@@ -278,8 +286,11 @@
<ButtonMenu <ButtonMenu
selected={selectedRegionId} selected={selectedRegionId}
autoSelectionIfOne autoSelectionIfOne
title={regionInfo.find((it) => it.region === selectedRegionId)?.name} title={selectedRegionName}
items={regionInfo.map((it) => ({ id: it.region === '' ? '#' : it.region, label: getEmbeddedLabel(it.name) }))} items={regionInfo.map((it) => ({
id: it.region === '' ? '#' : it.region,
label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
}))}
on:selected={(it) => { on:selected={(it) => {
selectedRegionId = it.detail === '#' ? '' : it.detail selectedRegionId = it.detail === '#' ? '' : it.detail
}} }}
@@ -291,12 +302,13 @@
{#each Object.keys(dayRanges) as k} {#each Object.keys(dayRanges) as k}
{@const v = groupped.get(k) ?? []} {@const v = groupped.get(k) ?? []}
{@const hasMore = (groupped.get(k) ?? []).length > limit} {@const hasMore = (groupped.get(k) ?? []).length > limit}
{@const activeV = v.filter((it) => it.mode === 'active' && (it.region ?? '') !== selectedRegionId)} {@const activeV = v.filter((it) => isActiveMode(it.mode) && it.region !== selectedRegionId)}
{@const archivedV = v.filter((it) => it.mode === 'archived')} {@const activeAll = v.filter((it) => isActiveMode(it.mode))}
{@const deletedV = v.filter((it) => it.mode === 'deleted')} {@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
{@const av = v.length - archivedV.length - deletedV.length} {@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
{@const maintenance = v.length - activeAll.length - archivedV.length - deletedV.length}
{#if v.length > 0} {#if v.length > 0}
<Expandable expandable={true} bordered={true}> <Expandable expandable={true} bordered={true} expanded={search.trim().length > 0}>
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<span class="fs-title focused-button flex-row-center"> <span class="fs-title focused-button flex-row-center">
{k} - {k} -
@@ -305,8 +317,8 @@
{:else} {:else}
{v.length} {v.length}
{/if} {/if}
{#if av > 0} {#if maintenance > 0}
- maitenance: {av} - maitenance: {maintenance}
{/if} {/if}
</span> </span>
</svelte:fragment> </svelte:fragment>
@@ -324,18 +336,18 @@
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="tools"> <svelte:fragment slot="tools">
{#if archivedV.length > 0} {#if activeAll.length > 0}
<Button <Button
icon={IconStop} icon={IconStop}
label={getEmbeddedLabel(`Mass Archive ${archivedV.length}`)} label={getEmbeddedLabel(`Mass Archive ${activeAll.length}`)}
kind={'ghost'} kind={'ghost'}
on:click={() => { on:click={() => {
showPopup(MessageBox, { showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Archive ${archivedV.length}`), label: getEmbeddedLabel(`Mass Archive ${activeAll.length}`),
message: getEmbeddedLabel(`Please confirm archive ${archivedV.length} workspaces`), message: getEmbeddedLabel(`Please confirm archive ${activeAll.length} workspaces`),
action: async () => { action: async () => {
void performWorkspaceOperation( void performWorkspaceOperation(
archivedV.map((it) => it.uuid), activeAll.map((it) => it.uuid),
'archive' 'archive'
) )
} }
@@ -351,8 +363,8 @@
label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)} label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)}
on:click={() => { on:click={() => {
showPopup(MessageBox, { showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Migrate ${archivedV.length}`), label: getEmbeddedLabel(`Mass Migrate ${activeV.length}`),
message: getEmbeddedLabel(`Please confirm migrate ${archivedV.length} workspaces`), message: getEmbeddedLabel(`Please confirm migrate ${activeV.length} workspaces`),
action: async () => { action: async () => {
await performWorkspaceOperation( await performWorkspaceOperation(
activeV.map((it) => it.uuid), activeV.map((it) => it.uuid),
@@ -371,38 +383,36 @@
{@const bIdx = backupIdx.get(workspace.uuid)} {@const bIdx = backupIdx.get(workspace.uuid)}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex fs-title cursor-pointer focused-button bordered"> <div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.uuid}`}>
<div class="flex p-2"> <Grid column={10} equalHeight={false} columnGap={0}>
<span class="label overflow-label flex-row-center" style:width={'12rem'}> <span class="label overflow-label flex-row-center flex-grow flex-shrink">
{wsName} {wsName}
<div class="ml-1"> <div class="ml-1">
<Button icon={IconOpen} size={'small'} on:click={() => select(workspace.url)} /> <Button icon={IconOpen} size={'small'} on:click={() => select(workspace.url)} />
</div> </div>
</span> </span>
<div class="ml-1" style:width={'12rem'}> <div class="ml-1">
{workspace.createdBy} <!-- Retrieve created by-->
<!-- {workspace.createdBy} -->
</div> </div>
<span class="label overflow-label" style:width={'8rem'}> <span class="label overflow-label flex-shrink p-1">
{workspace.region ?? ''} {workspace.region ?? ''}
</span> </span>
<span class="label overflow-label" style:width={'5rem'}> <span class="label overflow-label flex-shrink p-1">
{lastUsageDays} days {lastUsageDays} days
</span> </span>
<span class="label overflow-label p-1">
<span class="label overflow-label" style:width={'10rem'}>
{workspace.mode ?? '-'} {workspace.mode ?? '-'}
</span> </span>
<span class="label overflow-label flex-shrink">
<span class="label overflow-label" style:width={'2rem'}> {workspace.processingAttempts}
{workspace.attempts}
</span> </span>
<span class="flex flex-shrink">
<span class="flex flex-between" style:width={'5rem'}>
{#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0} {#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
({workspace.processingProgress}%) ({workspace.processingProgress}%)
{/if} {/if}
</span> </span>
<span class="flex flex-between" style:width={'5rem'}> <span class="flex flex-shrink">
{#if workspace.backupInfo != null} {#if workspace.backupInfo != null}
{@const sz = Math.max( {@const sz = Math.max(
workspace.backupInfo.backupSize, workspace.backupInfo.backupSize,
@@ -419,7 +429,7 @@
[#{bIdx}] [#{bIdx}]
{/if} {/if}
</span> </span>
<span class="flex flex-between" style:width={'5rem'}> <span class="flex flex-shrink">
{#if workspace.backupInfo != null} {#if workspace.backupInfo != null}
{@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))} {@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))}
@@ -430,82 +440,81 @@
{/if} {/if}
{/if} {/if}
</span> </span>
</div> <div class="flex flex-grow gap-1-5 flex-shrink">
<div class="flex flex-grow gap-1-5 flex-between"> <div class="flex flex-row-center gap-1-5">
<div class="flex flex-row-center gap-1-5"> {#if workspace.mode === 'active'}
{#if workspace.mode === 'active'} <Button
<Button icon={IconStop}
icon={IconStop} size={'small'}
size={'small'} label={getEmbeddedLabel('Archive')}
label={getEmbeddedLabel('Archive')} kind={'ghost'}
kind={'ghost'} on:click={() => {
on:click={() => { showPopup(MessageBox, {
showPopup(MessageBox, { label: getEmbeddedLabel(`Archive ${workspace.url}`),
label: getEmbeddedLabel(`Archive ${workspace.url}`), message: getEmbeddedLabel('Please confirm'),
message: getEmbeddedLabel('Please confirm'), action: async () => {
action: async () => { await performWorkspaceOperation(workspace.uuid, 'archive')
await performWorkspaceOperation(workspace.uuid, 'archive') }
} })
}) }}
}} />
/> {/if}
{/if}
{#if workspace.mode === 'archived'} {#if workspace.mode === 'archived'}
<Button <Button
icon={IconStart} icon={IconStart}
size={'small'} size={'small'}
kind={'ghost'} kind={'ghost'}
label={getEmbeddedLabel('Unarchive')} label={getEmbeddedLabel('Unarchive')}
on:click={() => { on:click={() => {
showPopup(MessageBox, { showPopup(MessageBox, {
label: getEmbeddedLabel(`Unarchive ${workspace.url}`), label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'), message: getEmbeddedLabel('Please confirm'),
action: async () => { action: async () => {
await performWorkspaceOperation(workspace.uuid, 'unarchive') await performWorkspaceOperation(workspace.uuid, 'unarchive')
} }
}) })
}} }}
/> />
{/if} {/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} {#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
<Button <Button
icon={IconArrowRight} icon={IconStop}
size={'small'} size={'small'}
kind={'positive'} kind={'dangerous'}
label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))} label={getEmbeddedLabel('Delete')}
on:click={() => { on:click={() => {
showPopup(MessageBox, { showPopup(MessageBox, {
label: getEmbeddedLabel(`Migrate ${workspace.url}`), label: getEmbeddedLabel(`Delete ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'), message: getEmbeddedLabel('Please confirm'),
action: async () => { action: async () => {
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId) await performWorkspaceOperation(workspace.uuid, 'delete')
} }
}) })
}} }}
/> />
{/if} {/if}
</div>
{#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> </div>
{/each} {/each}
</Expandable> </Expandable>
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Otevřít v postranním panelu na nové kartě", "OpenInSidebarNewTab": "Otevřít v postranním panelu na nové kartě",
"ConfigureWidgets": "Nastavit widgety", "ConfigureWidgets": "Nastavit widgety",
"Tab": "Karta", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "In neuer Seitenleiste öffnen", "OpenInSidebarNewTab": "In neuer Seitenleiste öffnen",
"ConfigureWidgets": "Widgets konfigurieren", "ConfigureWidgets": "Widgets konfigurieren",
"Tab": "Tab", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Open in sidebar new tab", "OpenInSidebarNewTab": "Open in sidebar new tab",
"ConfigureWidgets": "Configure widgets", "ConfigureWidgets": "Configure widgets",
"Tab": "Tab", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral", "OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
"ConfigureWidgets": "Configurar widgets", "ConfigureWidgets": "Configurar widgets",
"Tab": "Pestaña", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale", "OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
"ConfigureWidgets": "Configurer les widgets", "ConfigureWidgets": "Configurer les widgets",
"Tab": "Onglet", "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..."
} }
} }
+2 -1
View File
@@ -29,6 +29,7 @@
"MobileNotSupported": "Spiacenti, il supporto per i dispositivi mobili arriverà presto. Nel frattempo, si prega di utilizzare un computer desktop", "MobileNotSupported": "Spiacenti, il supporto per i dispositivi mobili arriverà presto. Nel frattempo, si prega di utilizzare un computer desktop",
"LogInAnyway": "Accedi comunque", "LogInAnyway": "Accedi comunque",
"WorkspaceCreating": "Creazione in corso...", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral", "OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
"ConfigureWidgets": "Configurar widgets", "ConfigureWidgets": "Configurar widgets",
"Tab": "Aba", "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..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели", "OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
"ConfigureWidgets": "Настроить виджеты", "ConfigureWidgets": "Настроить виджеты",
"Tab": "Вкладка", "Tab": "Вкладка",
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..." "WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления...",
"WorkspaceIsMigrating": "Рабочее пространство обновляется. Пожалуйста, подождите..."
} }
} }
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "在侧边栏新标签页中打开", "OpenInSidebarNewTab": "在侧边栏新标签页中打开",
"ConfigureWidgets": "配置小部件", "ConfigureWidgets": "配置小部件",
"Tab": "选项卡", "Tab": "选项卡",
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复..." "WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复...",
"WorkspaceIsMigrating": "工作区正在更新。请稍候..."
} }
} }
@@ -213,9 +213,9 @@
<div class="p-3">2.</div> <div class="p-3">2.</div>
<Button <Button
icon={IconArrowRight} icon={IconArrowRight}
label={getEmbeddedLabel('Reboot server')} label={getEmbeddedLabel('Reboot workspace')}
on:click={() => { 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' method: 'PUT'
}) })
}} }}
@@ -243,6 +243,17 @@ export async function connect (title: string): Promise<Client | undefined> {
onArchived: () => { onArchived: () => {
translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => { translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => {
versionError.set(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. // We need to refresh all active live queries and clear old queries.
+2 -1
View File
@@ -256,7 +256,8 @@ export default plugin(workbenchId, {
OpenInSidebar: '' as IntlString, OpenInSidebar: '' as IntlString,
OpenInSidebarNewTab: '' as IntlString, OpenInSidebarNewTab: '' as IntlString,
ConfigureWidgets: '' as IntlString, ConfigureWidgets: '' as IntlString,
WorkspaceIsArchived: '' as IntlString WorkspaceIsArchived: '' as IntlString,
WorkspaceIsMigrating: '' as IntlString
}, },
icon: { icon: {
Search: '' as Asset Search: '' as Asset
+5
View File
@@ -1140,6 +1140,11 @@
"projectFolder": "tests/sanity", "projectFolder": "tests/sanity",
"shouldPublish": false "shouldPublish": false
}, },
{
"packageName": "@hcengineering/tests-ws-sanity",
"projectFolder": "ws-tests/sanity",
"shouldPublish": false
},
{ {
"packageName": "@hcengineering/qms-tests-sanity", "packageName": "@hcengineering/qms-tests-sanity",
"projectFolder": "qms-tests/sanity", "projectFolder": "qms-tests/sanity",
-1
View File
@@ -241,7 +241,6 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection<WorkspaceS
for (const key of Object.keys(query)) { for (const key of Object.keys(query)) {
const qVal = (query as any)[key] const qVal = (query as any)[key]
if (key === 'workspaceUuid') { if (key === 'workspaceUuid') {
res.uuid = qVal res.uuid = qVal
} else { } else {
+44 -44
View File
@@ -15,22 +15,22 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { import {
AccountRole,
concatLink, concatLink,
Data, Data,
isActiveMode,
isWorkspaceCreating, isWorkspaceCreating,
MeasureContext, MeasureContext,
SocialIdType,
systemAccountUuid, systemAccountUuid,
Version, Version,
SocialIdType,
type BackupStatus, type BackupStatus,
type Branding, type Branding,
AccountRole,
type Person, type Person,
type WorkspaceMemberInfo,
type PersonUuid, type PersonUuid,
isActiveMode, type WorkspaceMemberInfo,
type WorkspaceUuid, type WorkspaceMode,
type WorkspaceMode type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import platform, { import platform, {
getMetadata, getMetadata,
@@ -45,48 +45,48 @@ import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
import { accountPlugin } from './plugin' import { accountPlugin } from './plugin'
import type { import type {
AccountDB, AccountDB,
AccountMethodHandler,
LoginInfo, LoginInfo,
OtpInfo, OtpInfo,
RegionInfo, RegionInfo,
WorkspaceEvent,
WorkspaceOperation,
AccountMethodHandler,
WorkspaceLoginInfo,
WorkspaceInfoWithStatus,
WorkspaceStatus,
SocialId, SocialId,
Workspace Workspace,
WorkspaceEvent,
WorkspaceInfoWithStatus,
WorkspaceLoginInfo,
WorkspaceOperation,
WorkspaceStatus
} from './types' } from './types'
import { import {
checkInvite,
cleanEmail, cleanEmail,
EndpointKind, confirmEmail,
getEndpoint,
getRegions,
verifyPassword,
isOtpValid,
createAccount, createAccount,
createWorkspaceRecord,
doJoinByInvite,
EndpointKind,
getAccount,
getEmailSocialId,
getEndpoint,
getFrontUrl,
getRegions,
getRolePower,
getSesUrl,
getSocialIdByKey,
getWorkspaceById,
getWorkspaceInfoWithStatusById,
getWorkspaceInvite,
getWorkspaces,
getWorkspacesInfoWithStatusByIds,
GUEST_ACCOUNT,
isOtpValid,
selectWorkspace,
sendEmailConfirmation,
sendOtp, sendOtp,
setPassword, setPassword,
wrap,
createWorkspaceRecord,
checkInvite,
GUEST_ACCOUNT,
getWorkspaceById,
getWorkspaceInvite,
getRolePower,
sendEmailConfirmation,
getEmailSocialId,
confirmEmail,
getAccount,
getSesUrl,
getFrontUrl,
getWorkspaceInfoWithStatusById,
signUpByEmail, signUpByEmail,
selectWorkspace, verifyPassword,
doJoinByInvite, wrap
getWorkspacesInfoWithStatusByIds,
getSocialIdByKey,
getWorkspaces
} from './utils' } from './utils'
// Move to config? // Move to config?
@@ -857,7 +857,7 @@ export async function performWorkspaceOperation (
): Promise<boolean> { ): Promise<boolean> {
const { extra } = decodeTokenVerbose(ctx, token) const { extra } = decodeTokenVerbose(ctx, token)
if (extra?.service !== 'admin') { if (extra?.admin !== 'true') {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
} }
@@ -874,7 +874,7 @@ export async function performWorkspaceOperation (
switch (event) { switch (event) {
case 'delete': case 'delete':
if (workspace.status.mode !== 'active') { 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' update.mode = 'pending-deletion'
@@ -883,7 +883,7 @@ export async function performWorkspaceOperation (
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
break break
case 'archive': case 'archive':
if (isActiveMode(workspace.status.mode)) { if (!isActiveMode(workspace.status.mode)) {
throw new PlatformError(unknownError('Archiving allowed only for active workspaces')) throw new PlatformError(unknownError('Archiving allowed only for active workspaces'))
} }
@@ -921,7 +921,7 @@ export async function performWorkspaceOperation (
update.mode = 'migration-pending-backup' update.mode = 'migration-pending-backup'
// NOTE: will only work for Mongo accounts // NOTE: will only work for Mongo accounts
;(update as any).targetRegion = params[0] update.targetRegion = params[0]
update.processingAttempts = 0 update.processingAttempts = 0
update.processingProgress = 0 update.processingProgress = 0
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
@@ -1278,7 +1278,7 @@ export async function updateWorkspaceInfo (
update.processingProgress = progress update.processingProgress = progress
break break
case 'migrate-clean-done': case 'migrate-clean-done':
wsUpdate.region = (workspace as any).targetRegion ?? '' wsUpdate.region = workspace.status.targetRegion ?? ''
update.mode = 'pending-restore' update.mode = 'pending-restore'
update.processingProgress = progress update.processingProgress = progress
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
@@ -1323,8 +1323,8 @@ export async function updateWorkspaceInfo (
await db.workspaceStatus.updateOne( await db.workspaceStatus.updateOne(
{ workspaceUuid: workspace.uuid }, { workspaceUuid: workspace.uuid },
{ {
...update, lastProcessingTime: Date.now(), // Some operations override it.
lastProcessingTime: Date.now() ...update
} }
) )
+2
View File
@@ -87,6 +87,8 @@ export interface WorkspaceStatus {
processingAttempts?: number processingAttempts?: number
processingMessage?: string processingMessage?: string
backupInfo?: BackupStatus backupInfo?: BackupStatus
targetRegion?: string
} }
export interface Workspace { export interface Workspace {
-15
View File
@@ -27,8 +27,6 @@ import {
systemAccountUuid, systemAccountUuid,
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore, type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
isActiveMode, isActiveMode,
isArchivingMode,
isMigrationMode,
type PersonUuid type PersonUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later 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 })) 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 { return {
-4
View File
@@ -92,8 +92,6 @@ export async function backupWorkspace (
externalStorage: StorageAdapter externalStorage: StorageAdapter
) => DbConfiguration, ) => DbConfiguration,
region: string, region: string,
freshBackup: boolean = false,
clean: boolean = false,
downloadLimit: number, downloadLimit: number,
contextVars: Record<string, any>, contextVars: Record<string, any>,
@@ -129,8 +127,6 @@ export async function backupWorkspace (
return getConfig(ctx, mainDbUrl, workspace, branding, externalStorage) return getConfig(ctx, mainDbUrl, workspace, branding, externalStorage)
}, },
region, region,
freshBackup,
clean,
downloadLimit, downloadLimit,
[], [],
contextVars contextVars
+4 -39
View File
@@ -672,8 +672,6 @@ export async function backup (
include?: Set<string> include?: Set<string>
skipDomains: string[] skipDomains: string[]
force: boolean 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 timeout: number
connectTimeout: number connectTimeout: number
skipBlobContentTypes: string[] skipBlobContentTypes: string[]
@@ -687,8 +685,6 @@ export async function backup (
token?: string token?: string
} = { } = {
force: false, force: false,
freshBackup: false,
clean: false,
timeout: 0, timeout: 0,
skipDomains: [], skipDomains: [],
connectTimeout: 30000, connectTimeout: 30000,
@@ -706,7 +702,6 @@ export async function backup (
ctx = ctx.newChild('backup', { ctx = ctx.newChild('backup', {
workspaceId, workspaceId,
force: options.force, force: options.force,
recheck: options.freshBackup,
timeout: options.timeout timeout: options.timeout
}) })
@@ -767,7 +762,7 @@ export async function backup (
let lastTxChecked = false let lastTxChecked = false
// Skip backup if there is no transaction changes. // Skip backup if there is no transaction changes.
if (options.getLastTx !== undefined && !options.freshBackup) { if (options.getLastTx !== undefined) {
lastTx = await options.getLastTx() lastTx = await options.getLastTx()
if (lastTx !== undefined) { if (lastTx !== undefined) {
if (lastTx._id === backupInfo.lastTxId && !options.force) { if (lastTx._id === backupInfo.lastTxId && !options.force) {
@@ -793,7 +788,7 @@ export async function backup (
? await options.getConnection() ? await options.getConnection()
: ((await createClient(transactorUrl, token, undefined, options.connectTimeout)) as CoreClient & BackupClient) : ((await createClient(transactorUrl, token, undefined, options.connectTimeout)) as CoreClient & BackupClient)
if (!lastTxChecked && !options.freshBackup) { if (!lastTxChecked) {
lastTx = await connection.findOne( lastTx = await connection.findOne(
core.class.Tx, core.class.Tx,
{ objectSpace: { $ne: core.space.Model } }, { objectSpace: { $ne: core.space.Model } },
@@ -932,7 +927,7 @@ export async function backup (
if (digest.delete(id as Ref<Doc>)) { if (digest.delete(id as Ref<Doc>)) {
oldHash.set(id as Ref<Doc>, currentHash) 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>)) { if (changes.updated.has(id as Ref<Doc>)) {
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>) removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
} }
@@ -1355,36 +1350,6 @@ export async function backup (
if (!canceled()) { if (!canceled()) {
backupInfo.lastTxId = lastTx?._id ?? '0' // We could store last tx, since full backup is complete 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 })) 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> => { const addFileSize = async (file: string | undefined | null): Promise<void> => {
@@ -1735,7 +1700,7 @@ export async function restore (
try { try {
let serverEndpoint = await getTransactorEndpoint(token, 'external') let serverEndpoint = await getTransactorEndpoint(token, 'external')
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://') 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' method: 'PUT'
}) })
} catch (err: any) { } catch (err: any) {
-8
View File
@@ -73,8 +73,6 @@ class BackupWorker {
) => DbConfiguration, ) => DbConfiguration,
readonly region: string, readonly region: string,
readonly contextVars: Record<string, any>, readonly contextVars: Record<string, any>,
readonly freshWorkspace: boolean = false,
readonly clean: boolean = false,
readonly skipDomains: string[] = [] readonly skipDomains: string[] = []
) {} ) {}
@@ -282,8 +280,6 @@ class BackupWorker {
backup(ctx, '', wsIds, storage, { backup(ctx, '', wsIds, storage, {
skipDomains: this.skipDomains, skipDomains: this.skipDomains,
force: true, force: true,
freshBackup: this.freshWorkspace,
clean: this.clean,
timeout: this.config.Timeout * 1000, timeout: this.config.Timeout * 1000,
connectTimeout: 5 * 60 * 1000, // 5 minutes to, connectTimeout: 5 * 60 * 1000, // 5 minutes to,
blobDownloadLimit: this.downloadLimit, blobDownloadLimit: this.downloadLimit,
@@ -408,8 +404,6 @@ export async function doBackupWorkspace (
externalStorage: StorageAdapter externalStorage: StorageAdapter
) => DbConfiguration, ) => DbConfiguration,
region: string, region: string,
freshWorkspace: boolean,
clean: boolean,
downloadLimit: number, downloadLimit: number,
skipDomains: string[], skipDomains: string[],
contextVars: Record<string, any>, contextVars: Record<string, any>,
@@ -423,8 +417,6 @@ export async function doBackupWorkspace (
getConfig, getConfig,
region, region,
contextVars, contextVars,
freshWorkspace,
clean,
skipDomains skipDomains
) )
backupWorker.downloadLimit = downloadLimit backupWorker.downloadLimit = downloadLimit
+6 -6
View File
@@ -15,7 +15,6 @@
import { import {
type Account, type Account,
type PersonId,
type Branding, type Branding,
type Class, type Class,
type Doc, type Doc,
@@ -29,6 +28,8 @@ import {
type MeasureContext, type MeasureContext,
type ModelDb, type ModelDb,
type Obj, type Obj,
type PersonId,
type PersonUuid,
type Ref, type Ref,
type SearchOptions, type SearchOptions,
type SearchQuery, type SearchQuery,
@@ -39,10 +40,9 @@ import {
type Tx, type Tx,
type TxFactory, type TxFactory,
type TxResult, type TxResult,
type WorkspaceUuid,
type WorkspaceIds,
type WorkspaceDataId, type WorkspaceDataId,
type PersonUuid type WorkspaceIds,
type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import type { Asset, Resource } from '@hcengineering/platform' import type { Asset, Resource } from '@hcengineering/platform'
import type { LiveQuery } from '@hcengineering/query' import type { LiveQuery } from '@hcengineering/query'
@@ -649,8 +649,8 @@ export interface AddSessionActive {
} }
export type AddSessionResponse = export type AddSessionResponse =
| AddSessionActive | AddSessionActive
| { upgrade: true } | { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, archived?: boolean } | { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session
+8 -8
View File
@@ -54,6 +54,7 @@ import {
SessionManager, SessionManager,
StorageAdapter, StorageAdapter,
type SessionFactory, type SessionFactory,
type AddSessionResponse,
type ClientSessionCtx, type ClientSessionCtx,
type ConnectionSocket, type ConnectionSocket,
type Session, type Session,
@@ -355,11 +356,7 @@ class TSessionManager implements SessionManager {
rawToken: string, rawToken: string,
pipelineFactory: PipelineFactory, pipelineFactory: PipelineFactory,
sessionId: string | undefined sessionId: string | undefined
): Promise< ): Promise<AddSessionResponse> {
| { session: Session, context: MeasureContext, workspaceId: WorkspaceUuid }
| { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, archived?: boolean }
> {
const { workspace: workspaceUuid } = token const { workspace: workspaceUuid } = token
let workspaceInfo: WorkspaceInfoWithStatus | undefined let workspaceInfo: WorkspaceInfoWithStatus | undefined
@@ -376,15 +373,15 @@ class TSessionManager implements SessionManager {
if (isArchivingMode(workspaceInfo.mode)) { if (isArchivingMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users // 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)) { if (isMigrationMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users // 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)) { if (isRestoringMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users // 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') { 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> { async forceClose (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket): Promise<void> {
const ws = this.workspaces.get(wsId) const ws = this.workspaces.get(wsId)
if (ws !== undefined) { if (ws !== undefined) {
this.ctx.warn('force-close', { name: ws.workspaceName })
ws.upgrade = true // We need to similare upgrade to refresh all clients. ws.upgrade = true // We need to similare upgrade to refresh all clients.
ws.closing = this.closeAll(wsId, ws, 99, 'force-close', ignoreSocket) ws.closing = this.closeAll(wsId, ws, 99, 'force-close', ignoreSocket)
this.workspaces.delete(wsId) this.workspaces.delete(wsId)
await ws.closing await ws.closing
ws.closing = undefined ws.closing = undefined
} else {
this.ctx.warn('force-close-unknown', { wsId })
} }
} }
+1 -1
View File
@@ -362,7 +362,7 @@ export async function upgradeModel (
const token = generateToken(systemAccountUuid, wsIds.uuid, { service: 'tool', admin: 'true' }) const token = generateToken(systemAccountUuid, wsIds.uuid, { service: 'tool', admin: 'true' })
try { 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' method: 'PUT'
}) })
} catch (err: any) { } catch (err: any) {
+34 -12
View File
@@ -12,23 +12,28 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { getClient as getAccountClient } from '@hcengineering/account-client'
import { import {
getBranding,
isArchivingMode,
isMigrationMode,
isRestoringMode,
systemAccountUuid,
type BrandingMap, type BrandingMap,
type Data, type Data,
type MeasureContext, type MeasureContext,
type Tx, type Tx,
type Version, type Version,
type WorkspaceUpdateEvent,
isArchivingMode,
isMigrationMode,
isRestoringMode,
type WorkspaceInfoWithStatus, type WorkspaceInfoWithStatus,
getBranding, type WorkspaceUpdateEvent,
systemAccountUuid type WorkspaceUuid
} from '@hcengineering/core' } from '@hcengineering/core'
import { type MigrateOperation, type ModelLogger } from '@hcengineering/model' import { type MigrateOperation, type ModelLogger } from '@hcengineering/model'
import { getClient as getAccountClient } from '@hcengineering/account-client' import {
import { withRetryConnUntilSuccess, withRetryConnUntilTimeout } from '@hcengineering/server-client' getTransactorEndpoint,
withRetryConnUntilSuccess,
withRetryConnUntilTimeout
} from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token' import { generateToken } from '@hcengineering/server-token'
import { FileModelLogger, prepareTools } from '@hcengineering/server-tool' import { FileModelLogger, prepareTools } from '@hcengineering/server-tool'
import path from 'path' 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 ( private async doWorkspaceOperation (
ctx: MeasureContext, ctx: MeasureContext,
workspace: WorkspaceInfoWithStatus, workspace: WorkspaceInfoWithStatus,
@@ -454,6 +473,8 @@ export class WorkspaceWorker {
case 'archiving-pending-backup': case 'archiving-pending-backup':
case 'archiving-backup': { case 'archiving-backup': {
await sendEvent('archiving-backup-started', 0) await sendEvent('archiving-backup-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
if (await this.doBackup(ctx, workspace, opt, true)) { if (await this.doBackup(ctx, workspace, opt, true)) {
await sendEvent('archiving-backup-done', 100) await sendEvent('archiving-backup-done', 100)
} }
@@ -476,6 +497,7 @@ export class WorkspaceWorker {
case 'deleting': { case 'deleting': {
// We should remove DB, not storages. // We should remove DB, not storages.
await sendEvent('delete-started', 0) await sendEvent('delete-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
try { try {
await this.doCleanup(ctx, workspace, true) await this.doCleanup(ctx, workspace, true)
} catch (err: any) { } catch (err: any) {
@@ -489,6 +511,7 @@ export class WorkspaceWorker {
case 'migration-pending-backup': case 'migration-pending-backup':
case 'migration-backup': case 'migration-backup':
await sendEvent('migrate-backup-started', 0) await sendEvent('migrate-backup-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
if (await this.doBackup(ctx, workspace, opt, false)) { if (await this.doBackup(ctx, workspace, opt, false)) {
await sendEvent('migrate-backup-done', 100) await sendEvent('migrate-backup-done', 100)
} }
@@ -497,6 +520,7 @@ export class WorkspaceWorker {
case 'migration-clean': { case 'migration-clean': {
// We should remove DB, not storages. // We should remove DB, not storages.
await sendEvent('migrate-clean-started', 0) await sendEvent('migrate-clean-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
try { try {
await this.doCleanup(ctx, workspace, false) await this.doCleanup(ctx, workspace, false)
} catch (err: any) { } catch (err: any) {
@@ -590,8 +614,6 @@ export class WorkspaceWorker {
}) })
}, },
this.region, this.region,
archive,
archive,
50000, 50000,
['blob'], ['blob'],
sharedPipelineContextVars, sharedPipelineContextVars,
@@ -604,7 +626,7 @@ export class WorkspaceWorker {
} }
) )
if (result) { if (result) {
console.log('backup completed') ctx.info('backup completed')
return true return true
} }
} finally { } finally {
@@ -681,7 +703,7 @@ export class WorkspaceWorker {
} }
) )
if (result) { if (result) {
console.log('backup completed') ctx.info('restore completed')
return true return true
} }
} finally { } finally {
+15 -2
View File
@@ -225,7 +225,7 @@ export function startHttpServer (
} }
case 'force-close': { case 'force-close': {
const wsId = req.query.wsId as WorkspaceUuid const wsId = req.query.wsId as WorkspaceUuid
void sessions.forceClose(wsId) void sessions.forceClose(wsId ?? payload.workspace)
res.writeHead(200) res.writeHead(200)
res.end() res.end()
return return
@@ -433,7 +433,7 @@ export function startHttpServer (
if (webSocketData.session instanceof Promise) { if (webSocketData.session instanceof Promise) {
void webSocketData.session.then((s) => { void webSocketData.session.then((s) => {
if ('error' in s) { if ('error' in s) {
if (s.archived === true) { if (s.specialError === 'archived') {
cs.send( cs.send(
ctx, ctx,
{ {
@@ -446,6 +446,19 @@ export function startHttpServer (
false, false,
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 { } else {
cs.send( cs.send(
ctx, ctx,
+10 -2
View File
@@ -41,7 +41,15 @@ export class ApiEndpoint {
return token 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 token = await this.loginAndGetToken(username, password)
const url = this.baseUrl const url = this.baseUrl
const payload = { const payload = {
@@ -50,7 +58,7 @@ export class ApiEndpoint {
} }
const headers = this.getDefaultHeaders(token) const headers = this.getDefaultHeaders(token)
const response = await this.request.post(url, { data: payload, headers }) 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> { async createAccount (username: string, password: string, firstName: string, lastName: string): Promise<any> {
+2
View File
@@ -8,6 +8,8 @@ export * from './model/select-workspace-page'
export * from './utils' export * from './utils'
export * from './model/tracker/issues-page' export * from './model/tracker/issues-page'
export * from './model/tracker/issues-details-page' export * from './model/tracker/issues-details-page'
export * from './model/tracker/types'
export * from './model/tracker/common-tracker-page' export * from './model/tracker/common-tracker-page'
export * from './model/tracker/tracker-navigation-menu-page' export * from './model/tracker/tracker-navigation-menu-page'
export * from './model/profile/user-profile-page' export * from './model/profile/user-profile-page'
export * from './API/Api'
+5
View File
@@ -31,6 +31,11 @@ export class LoginPage {
await (await this.page.goto(`${PlatformURI}/login/login`))?.finished() 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> { async clickSignUp (): Promise<void> {
await this.linkSignUp().click() await this.linkSignUp().click()
} }
+1 -27
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test' import { test } from '@playwright/test'
import { LoginPage } from '../model/login-page' import { LoginPage } from '../model/login-page'
import { DefaultWorkspace, generateId, PlatformURI, PlatformUser } from '../utils' import { DefaultWorkspace, generateId, PlatformURI, PlatformUser } from '../utils'
import { SelectWorkspacePage } from '../model/select-workspace-page' 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 { IssuesDetailsPage } from '../model/tracker/issues-details-page'
import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page' import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page'
import { SignInJoinPage } from '../model/signin-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', () => { test.describe('Workspace tests', () => {
let loginPage: LoginPage let loginPage: LoginPage
@@ -20,7 +18,6 @@ test.describe('Workspace tests', () => {
let leftSideMenuPage: LeftSideMenuPage let leftSideMenuPage: LeftSideMenuPage
let trackerNavigationMenuPage: TrackerNavigationMenuPage let trackerNavigationMenuPage: TrackerNavigationMenuPage
let issuesPage: IssuesPage let issuesPage: IssuesPage
let userProfilePage: UserProfilePage
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page) loginPage = new LoginPage(page)
@@ -29,7 +26,6 @@ test.describe('Workspace tests', () => {
leftSideMenuPage = new LeftSideMenuPage(page) leftSideMenuPage = new LeftSideMenuPage(page)
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page) trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
issuesPage = new IssuesPage(page) issuesPage = new IssuesPage(page)
userProfilePage = new UserProfilePage(page)
}) })
test('Create a workspace with a custom name', async () => { 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()
})
}) })
+5
View File
@@ -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
+33
View File
@@ -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"
}
]
}
}
+9
View File
@@ -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
+4
View File
@@ -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
+264
View File
@@ -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
+24
View File
@@ -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
+5
View File
@@ -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
+8
View File
@@ -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
+3
View File
@@ -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"
+11
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+5
View File
@@ -0,0 +1,5 @@
playwright-report
test-results/*
allure-report
allure-results
.auth
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "default"
}
+57
View File
@@ -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"
}
}
+42
View File
@@ -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 })
})
}
View File
+15
View File
@@ -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)
})
})
})
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./tests",
"outDir": "./lib",
"declarationDir": "./types",
"lib": ["esnext", "dom"],
}
}
+16
View File
@@ -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 $@
+17
View File
@@ -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