UBERF-9457: Region move fixes + tests (#7986)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-12 09:15:34 +07:00
committed by GitHub
parent 64e757eb63
commit ca73f4e8f1
67 changed files with 1459 additions and 214 deletions
+90 -51
View File
@@ -40,6 +40,8 @@ env:
rush.json
.prettierrc
tools
workers
ws-tests
PublishTempFolder: publish_artifacts
INIT_SCRIPTS_BRANCH: 'unified-init-scripts'
@@ -81,13 +83,13 @@ jobs:
run: node common/scripts/install-run-rush.js model-version
- name: Building...
run: node common/scripts/install-run-rush.js build -p 20
run: node common/scripts/install-run-rush.js build
- name: Bundle...
run: node common/scripts/install-run-rush.js bundle -p 20
run: node common/scripts/install-run-rush.js bundle
- name: Validate...
run: node common/scripts/install-run-rush.js validate -p 20
run: node common/scripts/install-run-rush.js validate
- name: Cache build results
uses: actions/cache@v4
@@ -154,7 +156,7 @@ jobs:
[ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ]
echo '================================================================'
- name: Formatting...
run: node common/scripts/install-run-rush.js fast-format
run: node common/scripts/install-run-rush.js fast-format --branch ${{ github.base_ref }}
- name: Check files formatting
run: |
echo '================================================================'
@@ -223,7 +225,7 @@ jobs:
run: node common/scripts/install-run-rush.js install
- name: Docker Build
run: node common/scripts/install-run-rush.js docker:build -p 20
run: node common/scripts/install-run-rush.js docker
env:
DOCKER_CLI_HINTS: false
- name: Prepare server
@@ -260,10 +262,8 @@ jobs:
run: |
cd ./tests/sanity
mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
docker logs $(docker ps | grep collaborator | cut -f 1 -d ' ') > logs/collaborator.log
cd logs
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
@@ -340,7 +340,7 @@ jobs:
run: node common/scripts/install-run-rush.js install
- name: Docker Build
run: node common/scripts/install-run-rush.js docker:build -p 20
run: node common/scripts/install-run-rush.js docker
env:
DOCKER_CLI_HINTS: false
- name: Prepare server
@@ -361,9 +361,8 @@ jobs:
run: |
cd ./tests/sanity
mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
cd logs
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
@@ -405,12 +404,12 @@ jobs:
run: node common/scripts/install-run-rush.js install
- name: Docker Build
run: node common/scripts/install-run-rush.js docker:build -p 20
run: node common/scripts/install-run-rush.js docker
env:
DOCKER_CLI_HINTS: false
- name: Configure /etc/hosts
run: |
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
- name: Prepare server
run: |
cd ./qms-tests
@@ -429,50 +428,90 @@ jobs:
run: |
cd ./qms-tests/sanity
mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
cd logs
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-qms
path: ./qms-tests/sanity/playwright-report/
# - name: Get Allure history
# uses: actions/checkout@v4
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
# continue-on-error: true
# with:
# ref: gh-pages
# path: gh-pages
# - name: Generates Allure Report
# uses: simple-elf/allure-report-action@master
# if: always()
# id: allure-report
# with:
# allure_results: ./qms-tests/sanity/allure-results/
# gh_pages: gh-pages
# allure_report: allure-report
# allure_history: allure-history
# - name: Upload allure test results
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: allure-report-qms
# path: ./allure-report/
# - name: Deploy report to Github Pages
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
# uses: peaceiris/actions-gh-pages@v4
# with:
# PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# PUBLISH_BRANCH: gh-pages
# PUBLISH_DIR: allure-history
- name: Upload Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: docker-logs-qms
path: ./qms-tests/sanity/logs
uitest-workspaces:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: tree:0
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-platform
with:
path: |
common/temp
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Checking for mis-matching dependencies...
run: node common/scripts/install-run-rush.js check
- name: Installing...
run: node common/scripts/install-run-rush.js install
- name: Build sanity-tests suite
run: node common/scripts/install-run-rush.js build -t @hcengineering/tests-sanity
- name: Docker Build
run: node common/scripts/install-run-rush.js docker
env:
DOCKER_CLI_HINTS: false
- name: Configure /etc/hosts
run: |
sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts
- name: Prepare server
run: |
cd ./ws-tests
export DO_CLEAN=true
./prepare.sh
- name: Install Playwright
run: |
cd ./ws-tests/sanity
node ../../common/scripts/install-run-rushx.js ci
- name: Run UI tests
run: |
cd ./ws-tests/sanity
node ../../common/scripts/install-run-rushx.js uitest
- name: 'Store docker logs'
if: always()
run: |
cd ./ws-tests/sanity
mkdir logs
cd logs
docker ps -a --format '{{.Names}}' | xargs -I {} sh -c 'docker logs {} > {}_logs.log 2>&1'
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-ws
path: ./ws-tests/sanity/playwright-report/
- name: Upload Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: docker-logs-ws
path: ./ws-tests/sanity/logs
docker-build:
needs: [build, test, svelte-check, uitest]
runs-on: ubuntu-latest
@@ -531,9 +570,9 @@ jobs:
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
- name: Docker build love-agent
run: |
cd ./services/ai-bot/love-agent
pnpm install && pnpm build
pnpm docker:build -v
cd ./services/ai-bot/love-agent
pnpm install && pnpm build
pnpm docker:build -v
env:
DOCKER_CLI_HINTS: false
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
@@ -590,7 +629,7 @@ jobs:
- name: Model version from git tags
run: node common/scripts/install-run-rush.js model-version
- name: Package
run: node common/scripts/install-run-rush.js package --to desktop -p 20 -v
run: node common/scripts/install-run-rush.js package --to desktop -v
- name: Package JSON
run: |
cd desktop-package
+1 -1
View File
@@ -9,7 +9,7 @@ temp/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
tests/sanity/screenshots
*tests/sanity/screenshots
# Runtime data
*.pid
+65
View File
@@ -70,6 +70,38 @@
"cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector"
},
{
"name": "Debug server(Test)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"FULLTEXT_URL": "http://localhost:4710",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@host.docker.internal:26258/defaultdb?sslmode=disable",
// "GREEN_URL": "http://host.docker.internal:6767?token=secret",
"SERVER_PORT": "3335",
"METRICS_CONSOLE": "false",
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"SERVER_SECRET": "secret",
"ENABLE_CONSOLE": "true",
"COLLABORATOR_URL": "ws://localhost:3079",
"FRONT_URL": "http://localhost:8083",
"ACCOUNTS_URL": "http://localhost:3003",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"MODEL_VERSION": "0.6.435",
"STATS_URL": "http://host.docker.internal:4901"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"showAsyncStacks": true,
"outputCapture": "std",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector"
},
{
"name": "Debug Fulltext",
"type": "node",
@@ -131,6 +163,39 @@
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Account(Staging)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27018",
"DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"SERVER_SECRET": "secret",
"REGION_INFO":"|Mongo;pg|Postgres;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://host.docker.internal:3334;;,ws://host.docker.internal:3335;;europe",
"ACCOUNTS_URL": "http://localhost:3003",
"ACCOUNT_PORT": "3003",
"FRONT_URL": "http://localhost:8083",
"STATS_URL": "http://host.docker.internal:4901",
"SES_URL": "",
// "DB_NS": "account-2",
// "WS_LIVENESS_DAYS": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
// "DISABLE_SIGNUP": "true",
// "INIT_SCRIPT_URL": "https://raw.githubusercontent.com/hcengineering/init/main/script.yaml",
// "INIT_WORKSPACE": "onboarding",
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Stats",
"type": "node",
+11 -1
View File
@@ -318,7 +318,7 @@
"summary": "Format changed projects",
"description": "Format and autofix linting issues in changed projects",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/each-diff.sh rushx format --force"
"shellCommand": "./common/scripts/fast-format.sh"
},
{
"commandKind": "global",
@@ -354,6 +354,16 @@
"shortName": "-f",
"description": "Force formatting",
"associatedCommands": ["format"]
},
{
"parameterKind": "string",
"argumentName": "BRANCH",
"required": false,
"associatedPhases": [],
"shortName": "-b",
"longName": "--branch",
"description": "Force formatting of branch",
"associatedCommands": ["fast-format"]
}
// {
// /**
+29
View File
@@ -1084,6 +1084,9 @@ importers:
'@rush-temp/tests-sanity':
specifier: file:./projects/tests-sanity.tgz
version: file:projects/tests-sanity.tgz
'@rush-temp/tests-ws-sanity':
specifier: file:./projects/tests-ws-sanity.tgz
version: file:projects/tests-ws-sanity.tgz
'@rush-temp/text':
specifier: file:./projects/text.tgz
version: file:projects/text.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
@@ -5156,6 +5159,10 @@ packages:
resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
version: 0.0.0
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
resolution: {integrity: sha512-2y+gKxse3ozzKeoi1TF7Qfv7Y1fWy9Z6HiqSsEeQgGPpv396cH7ZfpSJ/qwsxO66c6iq8LUuBtzZ4AlquHfG3w==, tarball: file:projects/tests-ws-sanity.tgz}
version: 0.0.0
'@rush-temp/text-core@file:projects/text-core.tgz':
resolution: {integrity: sha512-sQ1C/LTDJ6RoC7YHYOCrRGsOHerkeumxp9lRjRaTmjc65mAFtiizz4eYFlkbSVSQvahSdD5KMT1G+wvtmLq7Gg==, tarball: file:projects/text-core.tgz}
version: 0.0.0
@@ -24844,6 +24851,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
dependencies:
'@faker-js/faker': 8.4.1
'@playwright/test': 1.49.1
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
allure-js-commons: 3.0.7(allure-playwright@3.0.7(@playwright/test@1.49.1))
allure-playwright: 3.0.7(@playwright/test@1.49.1)
cross-env: 7.0.3
dotenv: 16.0.3
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
prettier: 3.2.5
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
'@rush-temp/text-core@file:projects/text-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)':
dependencies:
'@types/jest': 29.5.12
+3 -1
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
FILES=$(git diff origin/develop --name-only --diff-filter=ACMR | sed 's| |\\ |g')
branch=${BASE_BRANCH:-develop}
FILES=$(git diff origin/$branch --name-only --diff-filter=ACMR | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0
roots=$(rush list -p --json | grep "path" | cut -f 2 -d ':' | cut -f 2 -d '"')
+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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hcengineering/desktop",
"version": "0.6.271",
"version": "0.6.435",
"main": "dist/main/electron.js",
"template": "@hcengineering/webpack-package",
"scripts": {
-2
View File
@@ -42,8 +42,6 @@ async function doBackup (dirName: string, token: string, endpoint: string, works
ctx.info('do backup', { workspace, endpoint })
await backup(ctx, endpoint, wsid, storage, {
force: true,
freshBackup: false,
clean: false,
skipDomains: [],
timeout: 0,
connectTimeout: 60 * 1000,
+1
View File
@@ -10,6 +10,7 @@
"analyze": "rm -rf ./dist && cross-env NODE_ENV=production webpack --profile --json > stats.json",
"show": "webpack-bundle-analyzer stats.json dist",
"dev-server": "cross-env USE_CACHE=false CLIENT_TYPE=dev-server webpack serve",
"dev-server-test": "cross-env USE_CACHE=false CLIENT_TYPE=dev-server-test webpack serve",
"dev-worker": "cross-env USE_CACHE=false CLIENT_TYPE=dev-worker webpack serve",
"dev-worker-local": "cross-env USE_CACHE=false CLIENT_TYPE=dev-worker-local webpack serve",
"dev-server-cache": "cross-env USE_CACHE=true CLIENT_TYPE=dev-server webpack serve",
+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-bold': '/config.json',
'dev-server': '/config.json',
'dev-server-test': '/config-test.json',
'dev-worker': '/config-worker.json',
'dev-worker-local': '/config-worker-local.json'
}
+33 -1
View File
@@ -24,13 +24,14 @@ const mode = process.env.NODE_ENV || 'development'
const prod = mode === 'production'
const clientType = process.env.CLIENT_TYPE ?? ''
const devServer = clientType === 'dev-server'
const devServerTest = clientType === 'dev-server-test'
const devServerWorker = clientType === 'dev-worker'
const devServerWorkerLocal = clientType === 'dev-worker-local'
const devProduction = clientType === 'dev-production'
const devProductionHuly = clientType === 'dev-huly'
const devProductionBold = clientType === 'dev-bold'
const dev =
(process.env.CLIENT_TYPE ?? '') === 'dev' || devServer || devProduction || devProductionHuly || devProductionBold || devServerWorker || devServerWorkerLocal
(process.env.CLIENT_TYPE ?? '') === 'dev' || devServer || devProduction || devProductionHuly || devProductionBold || devServerWorker || devServerWorkerLocal || devServerTest
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const { EsbuildPlugin } = require('esbuild-loader')
@@ -71,6 +72,36 @@ const devProxy = {
}
}
const devProxyTest = {
'/account': {
target: 'http://localhost:3003',
changeOrigin: true,
pathRewrite: { '^/account': '' },
logLevel: 'debug'
},
'/files': {
target: 'http://localhost:8083',
changeOrigin: true,
logLevel: 'debug'
},
'/api/v1': {
target: 'http://localhost:8083',
changeOrigin: true,
logLevel: 'debug'
},
'/import': {
target: 'http://localhost:8083',
changeOrigin: true,
logLevel: 'debug'
},
'/rekoni/recognize': {
target: 'http://localhost:4004',
changeOrigin: true,
pathRewrite: { '^/rekoni/recognize': '/recognize' },
logLevel: 'debug'
}
}
const devHulyProxy = {
'/account': {
target: 'https://account.huly.app/',
@@ -148,6 +179,7 @@ const proxy = {
'dev-worker': devProxy,
'dev-worker-local': devProxy,
'dev-server': devProxy,
'dev-server-test': devProxyTest,
'dev-production': devFrontProxy,
'dev-bold': devBoldProxy,
'dev-huly': devHulyProxy
-10
View File
@@ -662,8 +662,6 @@ export function devTool (
})
},
cmd.region,
true,
true,
5000, // 5 gigabytes per blob
sharedPipelineContextVars,
async (storage, workspaceStorage) => {
@@ -734,8 +732,6 @@ export function devTool (
})
},
cmd.region,
false,
false,
100,
sharedPipelineContextVars
)
@@ -932,8 +928,6 @@ export function devTool (
)
.option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 15mb)', '15')
.option('-f, --force', 'Force backup', false)
.option('-f, --fresh', 'Force fresh backup', false)
.option('-c, --clean', 'Force clean of old backup files, only with fresh backup option', false)
.option('-t, --timeout <timeout>', 'Connect timeout in seconds', '30')
.action(
async (
@@ -942,8 +936,6 @@ export function devTool (
cmd: {
skip: string
force: boolean
fresh: boolean
clean: boolean
timeout: string
include: string
blobLimit: string
@@ -955,8 +947,6 @@ export function devTool (
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external')
await backup(toolCtx, endpoint, wsid, storage, {
force: cmd.force,
freshBackup: cmd.fresh,
clean: cmd.clean,
include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())),
skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()),
timeout: 0,
+1
View File
@@ -150,6 +150,7 @@ export default plugin(platformId, {
AccountNotConfirmed: '' as StatusCode<{ account: string }>,
WorkspaceNotFound: '' as StatusCode<{ workspace: string }>,
WorkspaceArchived: '' as StatusCode<{ workspace: string }>,
WorkspaceMigration: '' as StatusCode<{ workspace: string }>,
InvalidPassword: '' as StatusCode<{ account: string }>,
AccountAlreadyExists: '' as StatusCode<{ account: string }>,
AccountAlreadyConfirmed: '' as StatusCode<{ account: string }>,
@@ -281,6 +281,9 @@ class Connection implements ClientConnection {
if (resp.error?.code === platform.status.WorkspaceArchived) {
this.opt?.onArchived?.()
}
if (resp.error?.code === platform.status.WorkspaceMigration) {
this.opt?.onMigration?.()
}
}
if (resp.id !== undefined) {
+1
View File
@@ -62,6 +62,7 @@ export interface ClientFactoryOptions {
onUpgrade?: () => void
onUnauthorized?: () => void
onArchived?: () => void
onMigration?: () => void
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>
ctx?: MeasureContext
onDialTimeout?: () => void | Promise<void>
@@ -192,13 +192,19 @@
let selectedRegionId: string = ''
void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo?.filter((it) => it.name !== '') ?? []
regionInfo = _regionInfo ?? []
if (selectedRegionId === '' && regionInfo.length > 0) {
selectedRegionId = regionInfo[0].region
}
})
$: selectedRegionName = regionInfo.find((it) => it.region === selectedRegionId)?.name
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
$: selectedRegionName =
selectedRegionRef !== undefined
? selectedRegionRef.name.length > 0
? selectedRegionRef.name
: selectedRegionRef.region
: ''
$: byVersion = groupByArray(
workspaces.filter((it) => {
@@ -277,8 +283,11 @@
<ButtonMenu
selected={selectedRegionId}
autoSelectionIfOne
title={regionInfo.find((it) => it.region === selectedRegionId)?.name}
items={regionInfo.map((it) => ({ id: it.region === '' ? '#' : it.region, label: getEmbeddedLabel(it.name) }))}
title={selectedRegionName}
items={regionInfo.map((it) => ({
id: it.region === '' ? '#' : it.region,
label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
}))}
on:selected={(it) => {
selectedRegionId = it.detail === '#' ? '' : it.detail
}}
@@ -290,12 +299,13 @@
{#each Object.keys(dayRanges) as k}
{@const v = groupped.get(k) ?? []}
{@const hasMore = (groupped.get(k) ?? []).length > limit}
{@const activeV = v.filter((it) => it.mode === 'active' && (it.region ?? '') !== selectedRegionId)}
{@const archivedV = v.filter((it) => it.mode === 'archived')}
{@const deletedV = v.filter((it) => it.mode === 'deleted')}
{@const av = v.length - archivedV.length - deletedV.length}
{@const activeV = v.filter((it) => isActiveMode(it.mode) && it.region !== selectedRegionId)}
{@const activeAll = v.filter((it) => isActiveMode(it.mode))}
{@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
{@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
{@const maintenance = v.length - activeAll.length - archivedV.length - deletedV.length}
{#if v.length > 0}
<Expandable expandable={true} bordered={true}>
<Expandable expandable={true} bordered={true} expanded={search.trim().length > 0}>
<svelte:fragment slot="title">
<span class="fs-title focused-button flex-row-center">
{k} -
@@ -304,8 +314,8 @@
{:else}
{v.length}
{/if}
{#if av > 0}
- maitenance: {av}
{#if maintenance > 0}
- maitenance: {maintenance}
{/if}
</span>
</svelte:fragment>
@@ -323,18 +333,18 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="tools">
{#if archivedV.length > 0}
{#if activeAll.length > 0}
<Button
icon={IconStop}
label={getEmbeddedLabel(`Mass Archive ${archivedV.length}`)}
label={getEmbeddedLabel(`Mass Archive ${activeAll.length}`)}
kind={'ghost'}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Archive ${archivedV.length}`),
message: getEmbeddedLabel(`Please confirm archive ${archivedV.length} workspaces`),
label: getEmbeddedLabel(`Mass Archive ${activeAll.length}`),
message: getEmbeddedLabel(`Please confirm archive ${activeAll.length} workspaces`),
action: async () => {
void performWorkspaceOperation(
archivedV.map((it) => it.workspace),
activeAll.map((it) => it.workspace),
'archive'
)
}
@@ -350,8 +360,8 @@
label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Migrate ${archivedV.length}`),
message: getEmbeddedLabel(`Please confirm migrate ${archivedV.length} workspaces`),
label: getEmbeddedLabel(`Mass Migrate ${activeV.length}`),
message: getEmbeddedLabel(`Please confirm migrate ${activeV.length} workspaces`),
action: async () => {
await performWorkspaceOperation(
activeV.map((it) => it.workspace),
@@ -370,7 +380,7 @@
{@const bIdx = backupIdx.get(workspace.workspace)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex fs-title cursor-pointer focused-button bordered">
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.workspace}`}>
<div class="flex p-2">
<span class="label overflow-label flex-row-center" style:width={'12rem'}>
{wsName}
@@ -400,9 +410,6 @@
{workspace.attempts}
</span>
<!-- <span class="flex flex-between select-text overflow-label" style:width={'25rem'}>
{workspace.workspace}
</span> -->
<span class="flex flex-between" style:width={'5rem'}>
{#if workspace.progress !== 100 && workspace.progress !== 0}
({workspace.progress}%)
@@ -474,13 +481,12 @@
}}
/>
{/if}
{#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
<Button
icon={IconArrowRight}
size={'small'}
kind={'positive'}
label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))}
label={getEmbeddedLabel('Migrate')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Migrate ${workspace.workspaceUrl}`),
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Otevřít v postranním panelu na nové kartě",
"ConfigureWidgets": "Nastavit widgety",
"Tab": "Karta",
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení..."
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení...",
"WorkspaceIsMigrating": "Pracovní prostor je aktualizován. Prosím čekejte..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "In neuer Seitenleiste öffnen",
"ConfigureWidgets": "Widgets konfigurieren",
"Tab": "Tab",
"WorkspaceIsArchived": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung..."
"WorkspaceIsArchived": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung...",
"WorkspaceIsMigrating": "Workspace wird aktualisiert. Bitte warten..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Open in sidebar new tab",
"ConfigureWidgets": "Configure widgets",
"Tab": "Tab",
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore..."
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore...",
"WorkspaceIsMigrating": "Workspace is being updated. Please wait..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
"ConfigureWidgets": "Configurar widgets",
"Tab": "Pestaña",
"WorkspaceIsArchived": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo..."
"WorkspaceIsArchived": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo...",
"WorkspaceIsMigrating": "El espacio de trabajo se está actualizando. Por favor, espere..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
"ConfigureWidgets": "Configurer les widgets",
"Tab": "Onglet",
"WorkspaceIsArchived": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer..."
"WorkspaceIsArchived": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer...",
"WorkspaceIsMigrating": "L'espace de travail est en cours de mise à jour. Veuillez patienter..."
}
}
+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",
"LogInAnyway": "Accedi comunque",
"WorkspaceCreating": "Creazione in corso...",
"AccessDenied": "L'oggetto non esiste o non hai autorizzazione per accedervi."
"AccessDenied": "L'oggetto non esiste o non hai autorizzazione per accedervi.",
"WorkspaceIsMigrating": "Il workspace è in fase di aggiornamento. Attendi..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
"ConfigureWidgets": "Configurar widgets",
"Tab": "Aba",
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo..."
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo...",
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
"ConfigureWidgets": "Настроить виджеты",
"Tab": "Вкладка",
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..."
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления...",
"WorkspaceIsMigrating": "Рабочее пространство обновляется. Пожалуйста, подождите..."
}
}
+2 -1
View File
@@ -36,6 +36,7 @@
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
"ConfigureWidgets": "配置小部件",
"Tab": "选项卡",
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复..."
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复...",
"WorkspaceIsMigrating": "工作区正在更新。请稍候..."
}
}
@@ -213,9 +213,9 @@
<div class="p-3">2.</div>
<Button
icon={IconArrowRight}
label={getEmbeddedLabel('Reboot server')}
label={getEmbeddedLabel('Reboot workspace')}
on:click={() => {
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=reboot`, {
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
}}
@@ -209,6 +209,17 @@ export async function connect (title: string): Promise<Client | undefined> {
onArchived: () => {
translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
location.reload()
}, 5000)
})
},
onMigration: () => {
translateCB(plugin.string.WorkspaceIsMigrating, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
location.reload()
}, 5000)
})
},
// We need to refresh all active live queries and clear old queries.
+2 -1
View File
@@ -255,7 +255,8 @@ export default plugin(workbenchId, {
OpenInSidebar: '' as IntlString,
OpenInSidebarNewTab: '' as IntlString,
ConfigureWidgets: '' as IntlString,
WorkspaceIsArchived: '' as IntlString
WorkspaceIsArchived: '' as IntlString,
WorkspaceIsMigrating: '' as IntlString
},
icon: {
Search: '' as Asset
+5
View File
@@ -1135,6 +1135,11 @@
"projectFolder": "tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/tests-ws-sanity",
"projectFolder": "ws-tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/qms-tests-sanity",
"projectFolder": "qms-tests/sanity",
+9 -16
View File
@@ -32,7 +32,6 @@ import core, {
getWorkspaceId,
isActiveMode,
isArchivingMode,
isMigrationMode,
isWorkspaceCreating,
MeasureContext,
RateLimiter,
@@ -503,18 +502,6 @@ export async function selectWorkspace (
}
if (workspaceInfo !== null) {
if (isArchivingMode(workspaceInfo.mode) || isMigrationMode(workspaceInfo.mode)) {
const result: WorkspaceLoginInfo = {
endpoint: '',
email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace), getExtra(accountInfo)),
workspace: workspaceUrl,
workspaceId: workspaceInfo.workspace,
mode: workspaceInfo.mode,
progress: workspaceInfo.progress
}
return result
}
if (workspaceInfo.disabled === true && isActiveMode(workspaceInfo.mode)) {
ctx.error('workspace disabled', { workspaceUrl, email })
throw new PlatformError(
@@ -1594,7 +1581,9 @@ export async function createUserWorkspace (
endpoint: getEndpoint(ctx, workspaceInfo, EndpointKind.External),
email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace), getExtra(userAccount)),
workspace: workspaceInfo.workspaceUrl
workspace: workspaceInfo.workspaceUrl,
workspaceId: workspaceInfo.workspace,
workspaceName: workspaceInfo.workspaceName
}
ctx.info('Creating user side done', { workspaceName, email })
return result
@@ -2723,8 +2712,12 @@ export async function joinWithProvider (
const token = generateToken(email, getWorkspaceId(''), getExtra(account))
const ws = await getWorkspaceById(db, workspace.name)
if (ws != null && ws.accounts.includes(account._id)) {
if (ws == null) {
throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspace.name })
)
}
if (ws.accounts.includes(account._id)) {
const result = {
endpoint: getEndpoint(ctx, ws, EndpointKind.External),
email,
-4
View File
@@ -93,8 +93,6 @@ export async function backupWorkspace (
externalStorage: StorageAdapter
) => DbConfiguration,
region: string,
freshBackup: boolean = false,
clean: boolean = false,
downloadLimit: number,
contextVars: Record<string, any>,
@@ -130,8 +128,6 @@ export async function backupWorkspace (
return getConfig(ctx, mainDbUrl, workspace, branding, externalStorage)
},
region,
freshBackup,
clean,
downloadLimit,
[],
contextVars
+6 -45
View File
@@ -33,7 +33,6 @@ import core, {
SortingOrder,
systemAccountEmail,
toIdMap,
toWorkspaceString,
TxProcessor,
WorkspaceId,
type BackupStatus,
@@ -671,8 +670,6 @@ export async function backup (
include?: Set<string>
skipDomains: string[]
force: boolean
freshBackup: boolean // If passed as true, will download all documents except blobs as new backup
clean: boolean // If set will perform a clena of old backup files
timeout: number
connectTimeout: number
skipBlobContentTypes: string[]
@@ -686,8 +683,6 @@ export async function backup (
token?: string
} = {
force: false,
freshBackup: false,
clean: false,
timeout: 0,
skipDomains: [],
connectTimeout: 30000,
@@ -704,7 +699,6 @@ export async function backup (
ctx = ctx.newChild('backup', {
workspaceId: workspaceId.name,
force: options.force,
recheck: options.freshBackup,
timeout: options.timeout
})
@@ -760,7 +754,7 @@ export async function backup (
let lastTxChecked = false
// Skip backup if there is no transaction changes.
if (options.getLastTx !== undefined && !options.freshBackup) {
if (options.getLastTx !== undefined) {
lastTx = await options.getLastTx()
if (lastTx !== undefined) {
if (lastTx._id === backupInfo.lastTxId && !options.force) {
@@ -785,7 +779,7 @@ export async function backup (
? await options.getConnection()
: ((await createClient(transactorUrl, token, undefined, options.connectTimeout)) as CoreClient & BackupClient)
if (!lastTxChecked && !options.freshBackup) {
if (!lastTxChecked) {
lastTx = await connection.findOne(
core.class.Tx,
{ objectSpace: { $ne: core.space.Model } },
@@ -924,7 +918,7 @@ export async function backup (
if (digest.delete(id as Ref<Doc>)) {
oldHash.set(id as Ref<Doc>, currentHash)
}
if (currentHash !== serverDocHash || (options.freshBackup && domain !== DOMAIN_BLOB)) {
if (currentHash !== serverDocHash) {
if (changes.updated.has(id as Ref<Doc>)) {
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
}
@@ -1347,36 +1341,6 @@ export async function backup (
if (!canceled()) {
backupInfo.lastTxId = lastTx?._id ?? '0' // We could store last tx, since full backup is complete
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
if (options.freshBackup && options.clean) {
// Preparing a list of files to clean
ctx.info('Cleaning old backup files...')
for (const sn of backupInfo.snapshots.slice(0, backupInfo.snapshots.length - 1)) {
const filesToDelete: string[] = []
for (const [domain, dsn] of [...Object.entries(sn.domains)]) {
if (domain === DOMAIN_BLOB) {
continue
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (sn.domains as any)[domain]
filesToDelete.push(...(dsn.snapshots ?? []))
filesToDelete.push(...(dsn.storage ?? []))
if (dsn.snapshot !== undefined) {
filesToDelete.push(dsn.snapshot)
}
}
for (const file of filesToDelete) {
ctx.info('Removing file...', { file })
await storage.delete(file)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete sizeInfo[file]
}
}
ctx.info('Cleaning complete...')
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
}
}
const addFileSize = async (file: string | undefined | null): Promise<void> => {
@@ -1726,12 +1690,9 @@ export async function restore (
try {
let serverEndpoint = await getTransactorEndpoint(token, 'external')
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
await fetch(
serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${toWorkspaceString(workspaceId)}`,
{
method: 'PUT'
}
)
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
// Ignore
}
-8
View File
@@ -72,8 +72,6 @@ class BackupWorker {
) => DbConfiguration,
readonly region: string,
readonly contextVars: Record<string, any>,
readonly freshWorkspace: boolean = false,
readonly clean: boolean = false,
readonly skipDomains: string[] = []
) {}
@@ -280,8 +278,6 @@ class BackupWorker {
backup(ctx, '', getWorkspaceId(ws.workspace), storage, {
skipDomains: this.skipDomains,
force: true,
freshBackup: this.freshWorkspace,
clean: this.clean,
timeout: this.config.Timeout * 1000,
connectTimeout: 5 * 60 * 1000, // 5 minutes to,
blobDownloadLimit: this.downloadLimit,
@@ -406,8 +402,6 @@ export async function doBackupWorkspace (
externalStorage: StorageAdapter
) => DbConfiguration,
region: string,
freshWorkspace: boolean,
clean: boolean,
downloadLimit: number,
skipDomains: string[],
contextVars: Record<string, any>,
@@ -421,8 +415,6 @@ export async function doBackupWorkspace (
getConfig,
region,
contextVars,
freshWorkspace,
clean,
skipDomains
)
backupWorker.downloadLimit = downloadLimit
+14 -2
View File
@@ -15,6 +15,7 @@
import {
type Account,
type BaseWorkspaceInfo,
type Branding,
type Class,
type Doc,
@@ -637,10 +638,21 @@ export interface AddSessionActive {
context: MeasureContext
workspaceId: string
}
export interface WorkspaceLoginInfo extends Omit<BaseWorkspaceInfo, 'workspace'> {
upgrade?: {
toProcess: number
total: number
elapsed: number
eta: number
}
workspaceId: string
}
export type AddSessionResponse =
| AddSessionActive
| { upgrade: true }
| { error: any, terminate?: boolean, archived?: boolean }
| { upgrade: true, upgradeInfo?: WorkspaceLoginInfo['upgrade'] }
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
/**
* @public
+10 -20
View File
@@ -27,7 +27,6 @@ import core, {
versionToString,
withContext,
WorkspaceEvent,
type BaseWorkspaceInfo,
type Branding,
type BrandingMap,
type MeasureContext,
@@ -45,10 +44,12 @@ import {
ServerFactory,
SessionManager,
StorageAdapter,
type AddSessionResponse,
type ClientSessionCtx,
type ConnectionSocket,
type Session,
type Workspace
type Workspace,
type WorkspaceLoginInfo
} from '@hcengineering/server-core'
import { generateToken, type Token } from '@hcengineering/server-token'
@@ -57,16 +58,6 @@ import { sendResponse } from './utils'
const ticksPerSecond = 20
const workspaceSoftShutdownTicks = 15 * ticksPerSecond
interface WorkspaceLoginInfo extends Omit<BaseWorkspaceInfo, 'workspace'> {
upgrade?: {
toProcess: number
total: number
elapsed: number
eta: number
}
workspaceId: string
}
function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle: () => void } {
let timer: any
return {
@@ -330,11 +321,7 @@ class TSessionManager implements SessionManager {
rawToken: string,
pipelineFactory: PipelineFactory,
sessionId: string | undefined
): Promise<
| { session: Session, context: MeasureContext, workspaceId: string }
| { upgrade: true, upgradeInfo?: WorkspaceLoginInfo['upgrade'] }
| { error: any, terminate?: boolean, archived?: boolean }
> {
): Promise<AddSessionResponse> {
const wsString = toWorkspaceString(token.workspace)
let workspaceInfo: WorkspaceLoginInfo | undefined
@@ -352,15 +339,15 @@ class TSessionManager implements SessionManager {
if (isArchivingMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is archived'), terminate: true, archived: true }
return { error: new Error('Workspace is archived'), terminate: true, specialError: 'archived' }
}
if (isMigrationMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is in region migration'), terminate: true, archived: false }
return { error: new Error('Workspace is in region migration'), terminate: true, specialError: 'migration' }
}
if (isRestoringMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is in backup restore'), terminate: true, archived: false }
return { error: new Error('Workspace is in backup restore'), terminate: true, specialError: 'migration' }
}
if (workspaceInfo.disabled === true && token.email !== systemAccountEmail && token.extra?.admin !== 'true') {
@@ -806,11 +793,14 @@ class TSessionManager implements SessionManager {
async forceClose (wsId: string, ignoreSocket?: ConnectionSocket): Promise<void> {
const ws = this.workspaces.get(wsId)
if (ws !== undefined) {
this.ctx.warn('force-close', { name: ws.workspaceName })
ws.upgrade = true // We need to similare upgrade to refresh all clients.
ws.closing = this.closeAll(wsId, ws, 99, 'force-close', ignoreSocket)
this.workspaces.delete(wsId)
await ws.closing
ws.closing = undefined
} else {
this.ctx.warn('force-close-unknown', { wsId })
}
}
+3 -7
View File
@@ -27,7 +27,6 @@ import core, {
MigrationState,
ModelDb,
systemAccountEmail,
toWorkspaceString,
Tx,
TxOperations,
WorkspaceId,
@@ -357,12 +356,9 @@ export async function upgradeModel (
const serverEndpoint = transactorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://')
const token = generateToken(systemAccountEmail, workspaceId, { admin: 'true' })
try {
await fetch(
serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${toWorkspaceString(workspaceId)}`,
{
method: 'PUT'
}
)
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
// Ignore error if transactor is not yet ready
}
+23 -4
View File
@@ -19,6 +19,7 @@ import {
type MeasureContext,
type Tx,
type Version,
type WorkspaceId,
type WorkspaceUpdateEvent,
getBranding,
getWorkspaceId,
@@ -30,6 +31,7 @@ import {
import { type MigrateOperation, type ModelLogger } from '@hcengineering/model'
import {
getPendingWorkspace,
getTransactorEndpoint,
updateWorkspaceInfo,
withRetryConnUntilSuccess,
withRetryConnUntilTimeout,
@@ -399,6 +401,20 @@ export class WorkspaceWorker {
}
}
async sendTransactorMaitenance (token: string, ws: WorkspaceId): Promise<void> {
try {
let serverEndpoint = await getTransactorEndpoint(token)
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
console.log('sending event', serverEndpoint, ws.name)
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
console.log(err)
// Ignore
}
}
private async doWorkspaceOperation (
ctx: MeasureContext,
workspace: BaseWorkspaceInfo,
@@ -427,6 +443,8 @@ export class WorkspaceWorker {
case 'archiving-pending-backup':
case 'archiving-backup': {
await sendEvent('archiving-backup-started', 0)
await this.sendTransactorMaitenance(token, { name: workspace.workspace })
if (await this.doBackup(ctx, workspace, opt, true)) {
await sendEvent('archiving-backup-done', 100)
}
@@ -449,6 +467,7 @@ export class WorkspaceWorker {
case 'deleting': {
// We should remove DB, not storages.
await sendEvent('delete-started', 0)
await this.sendTransactorMaitenance(token, { name: workspace.workspace })
try {
await this.doCleanup(ctx, workspace, true)
} catch (err: any) {
@@ -462,6 +481,7 @@ export class WorkspaceWorker {
case 'migration-pending-backup':
case 'migration-backup':
await sendEvent('migrate-backup-started', 0)
await this.sendTransactorMaitenance(token, { name: workspace.workspace })
if (await this.doBackup(ctx, workspace, opt, false)) {
await sendEvent('migrate-backup-done', 100)
}
@@ -470,6 +490,7 @@ export class WorkspaceWorker {
case 'migration-clean': {
// We should remove DB, not storages.
await sendEvent('migrate-clean-started', 0)
await this.sendTransactorMaitenance(token, { name: workspace.workspace })
try {
await this.doCleanup(ctx, workspace, false)
} catch (err: any) {
@@ -556,8 +577,6 @@ export class WorkspaceWorker {
})
},
this.region,
archive,
archive,
50000,
['blob'],
sharedPipelineContextVars,
@@ -570,7 +589,7 @@ export class WorkspaceWorker {
}
)
if (result) {
console.log('backup completed')
ctx.info('backup completed')
return true
}
} finally {
@@ -633,7 +652,7 @@ export class WorkspaceWorker {
}
)
if (result) {
console.log('backup completed')
ctx.info('restore completed')
return true
}
} finally {
+15 -2
View File
@@ -185,7 +185,7 @@ export function startHttpServer (
}
case 'force-close': {
const wsId = req.query.wsId as string
void sessions.forceClose(wsId)
void sessions.forceClose(wsId ?? payload.workspace.name)
res.writeHead(200)
res.end()
return
@@ -374,7 +374,7 @@ export function startHttpServer (
if (webSocketData.session instanceof Promise) {
void webSocketData.session.then((s) => {
if ('error' in s) {
if (s.archived === true) {
if (s.specialError === 'archived') {
cs.send(
ctx,
{
@@ -387,6 +387,19 @@ export function startHttpServer (
false,
false
)
} else if (s.specialError === 'migration') {
cs.send(
ctx,
{
id: -1,
error: new Status(Severity.ERROR, platform.status.WorkspaceMigration, {
workspace: token.workspace.name
}),
terminate: s.terminate
},
false,
false
)
} else {
cs.send(
ctx,
+10 -2
View File
@@ -41,7 +41,15 @@ export class ApiEndpoint {
return token
}
async createWorkspaceWithLogin (workspaceName: string, username: string, password: string): Promise<any> {
async createWorkspaceWithLogin (
workspaceName: string,
username: string,
password: string
): Promise<{
workspace: string
workspaceId: string
workspaceName: string
}> {
const token = await this.loginAndGetToken(username, password)
const url = this.baseUrl
const payload = {
@@ -50,7 +58,7 @@ export class ApiEndpoint {
}
const headers = this.getDefaultHeaders(token)
const response = await this.request.post(url, { data: payload, headers })
return await response.json()
return (await response.json()).result
}
async createAccount (username: string, password: string, firstName: string, lastName: string): Promise<any> {
+2
View File
@@ -8,6 +8,8 @@ export * from './model/select-workspace-page'
export * from './utils'
export * from './model/tracker/issues-page'
export * from './model/tracker/issues-details-page'
export * from './model/tracker/types'
export * from './model/tracker/common-tracker-page'
export * from './model/tracker/tracker-navigation-menu-page'
export * from './model/profile/user-profile-page'
export * from './API/Api'
+5
View File
@@ -29,6 +29,11 @@ export class LoginPage {
await (await this.page.goto(`${PlatformURI}/login/login`))?.finished()
}
// ACTIONS
async gotoAdmin (): Promise<void> {
await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished()
}
async clickSignUp (): Promise<void> {
await this.linkSignUp().click()
}
+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
+29
View File
@@ -0,0 +1,29 @@
#!/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 set-user-admin admin true
./tool.sh create-account user1 -f John -l Appleseed -p 1234
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
./tool.sh confirm-email user1
./tool.sh confirm-email user2
+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,101 @@
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 workspaceInfo = await api.createWorkspaceWithLogin(generateId(), '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(workspaceInfo.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.workspaceId)
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Archive' }).click()
await page2.getByRole('button', { name: 'Ok' }).click()
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).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.workspaceId}"]`).getByRole('button', { name: 'Unarchive' }).click()
await page2.getByRole('button', { name: 'Ok' }).click()
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).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,227 @@
import { faker } from '@faker-js/faker'
import {
generateId,
IssuesDetailsPage,
IssuesPage,
LeftSideMenuPage,
LoginPage,
NewIssue,
SelectWorkspacePage,
SignInJoinPage,
SignUpData,
SignUpPage,
TrackerNavigationMenuPage,
UserProfilePage
} from '@hcengineering/tests-sanity'
import { 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 () => {
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 userProfilePage.checkIfAccountIsDisabled()
})
})
@@ -0,0 +1,91 @@
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 workspaceInfo = await api.createWorkspaceWithLogin(generateId(), '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(workspaceInfo.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.workspaceId)
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Migrate' }).click()
await page2.getByRole('button', { name: 'Ok' }).click()
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).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