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
.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",
@@ -132,6 +164,39 @@
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Account(Staging)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27018",
"DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"SERVER_SECRET": "secret",
"REGION_INFO":"|Mongo;pg|Postgres;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://host.docker.internal:3334;;,ws://host.docker.internal:3335;;europe",
"ACCOUNTS_URL": "http://localhost:3003",
"ACCOUNT_PORT": "3003",
"FRONT_URL": "http://localhost:8083",
"STATS_URL": "http://host.docker.internal:4901",
"SES_URL": "",
// "DB_NS": "account-2",
// "WS_LIVENESS_DAYS": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
// "DISABLE_SIGNUP": "true",
// "INIT_SCRIPT_URL": "https://raw.githubusercontent.com/hcengineering/init/main/script.yaml",
// "INIT_WORKSPACE": "onboarding",
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Stats",
"type": "node",
+11 -1
View File
@@ -331,7 +331,7 @@
"summary": "Format changed projects",
"description": "Format and autofix linting issues in changed projects",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/each-diff.sh rushx format --force"
"shellCommand": "./common/scripts/fast-format.sh"
},
{
"commandKind": "global",
@@ -367,6 +367,16 @@
"shortName": "-f",
"description": "Force formatting",
"associatedCommands": ["format"]
},
{
"parameterKind": "string",
"argumentName": "BRANCH",
"required": false,
"associatedPhases": [],
"shortName": "-b",
"longName": "--branch",
"description": "Force formatting of branch",
"associatedCommands": ["fast-format"]
}
// {
// /**
+29
View File
@@ -1087,6 +1087,9 @@ importers:
'@rush-temp/tests-sanity':
specifier: file:./projects/tests-sanity.tgz
version: file:projects/tests-sanity.tgz
'@rush-temp/tests-ws-sanity':
specifier: file:./projects/tests-ws-sanity.tgz
version: file:projects/tests-ws-sanity.tgz
'@rush-temp/text':
specifier: file:./projects/text.tgz
version: file:projects/text.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)
@@ -5166,6 +5169,10 @@ packages:
resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
version: 0.0.0
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
resolution: {integrity: sha512-2y+gKxse3ozzKeoi1TF7Qfv7Y1fWy9Z6HiqSsEeQgGPpv396cH7ZfpSJ/qwsxO66c6iq8LUuBtzZ4AlquHfG3w==, tarball: file:projects/tests-ws-sanity.tgz}
version: 0.0.0
'@rush-temp/text-core@file:projects/text-core.tgz':
resolution: {integrity: sha512-sQ1C/LTDJ6RoC7YHYOCrRGsOHerkeumxp9lRjRaTmjc65mAFtiizz4eYFlkbSVSQvahSdD5KMT1G+wvtmLq7Gg==, tarball: file:projects/text-core.tgz}
version: 0.0.0
@@ -24910,6 +24917,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@rush-temp/tests-ws-sanity@file:projects/tests-ws-sanity.tgz':
dependencies:
'@faker-js/faker': 8.4.1
'@playwright/test': 1.49.1
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
allure-js-commons: 3.0.7(allure-playwright@3.0.7(@playwright/test@1.49.1))
allure-playwright: 3.0.7(@playwright/test@1.49.1)
cross-env: 7.0.3
dotenv: 16.0.3
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
prettier: 3.2.5
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
'@rush-temp/text-core@file:projects/text-core.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))(utf-8-validate@6.0.4)':
dependencies:
'@types/jest': 29.5.12
+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
-2
View File
@@ -41,8 +41,6 @@ async function doBackup (dirName: string, token: string, endpoint: string, wsIds
ctx.info('do backup', { ...wsIds, endpoint })
await backup(ctx, endpoint, wsIds, storage, {
force: true,
freshBackup: false,
clean: false,
skipDomains: [],
timeout: 0,
connectTimeout: 60 * 1000,
+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
+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,
// true,
// true,
// 5000, // 5 gigabytes per blob
// sharedPipelineContextVars,
// async (storage, workspaceStorage) => {
@@ -661,8 +659,6 @@ export function devTool (
// })
// },
// cmd.region,
// false,
// false,
// 100,
// sharedPipelineContextVars
// )
@@ -859,8 +855,6 @@ export function devTool (
// )
// .option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 15mb)', '15')
// .option('-f, --force', 'Force backup', false)
// .option('-f, --fresh', 'Force fresh backup', false)
// .option('-c, --clean', 'Force clean of old backup files, only with fresh backup option', false)
// .option('-t, --timeout <timeout>', 'Connect timeout in seconds', '30')
// .action(
// async (
@@ -869,8 +863,6 @@ export function devTool (
// cmd: {
// skip: string
// force: boolean
// fresh: boolean
// clean: boolean
// timeout: string
// include: string
// blobLimit: string
@@ -882,8 +874,6 @@ export function devTool (
// const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external')
// await backup(toolCtx, endpoint, wsIds, storage, {
// force: cmd.force,
// freshBackup: cmd.fresh,
// clean: cmd.clean,
// include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';').map((it) => it.trim())),
// skipDomains: (cmd.skip ?? '').split(';').map((it) => it.trim()),
// timeout: 0,
+1
View File
@@ -150,6 +150,7 @@ export default plugin(platformId, {
AccountNotConfirmed: '' as StatusCode,
WorkspaceNotFound: '' as StatusCode<{ workspaceUuid?: string, workspaceName?: string, workspaceUrl?: string }>,
WorkspaceArchived: '' as StatusCode<{ workspaceUuid: string }>,
WorkspaceMigration: '' as StatusCode<{ workspaceUuid: string }>,
SocialIdNotFound: '' as StatusCode<{ socialId: string, type: string }>,
SocialIdNotConfirmed: '' as StatusCode<{ socialId: string, type: string }>,
SocialIdAlreadyConfirmed: '' as StatusCode<{ socialId: string, type: string }>,
@@ -283,6 +283,9 @@ class Connection implements ClientConnection {
if (resp.error?.code === platform.status.WorkspaceArchived) {
this.opt?.onArchived?.()
}
if (resp.error?.code === platform.status.WorkspaceMigration) {
this.opt?.onMigration?.()
}
}
if (resp.id !== undefined) {
+1
View File
@@ -63,6 +63,7 @@ export interface ClientFactoryOptions {
onUpgrade?: () => void
onUnauthorized?: () => void
onArchived?: () => void
onMigration?: () => void
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>
ctx?: MeasureContext
onDialTimeout?: () => void | Promise<void>
@@ -27,11 +27,13 @@
Scroller,
SearchEdit,
showPopup,
ticker
ticker,
Grid
} from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation } from '../utils'
import { RegionInfo } from '@hcengineering/account-client'
import ColStar from '@hcengineering/ui/src/components/icons/ColStar.svelte'
$: now = $ticker
@@ -44,7 +46,7 @@
window.open(url, '_blank')
}
type WorkspaceInfo = WorkspaceInfoWithStatus & { attempts: number }
type WorkspaceInfo = WorkspaceInfoWithStatus & { processingAttempts: number }
let workspaces: WorkspaceInfo[] = []
@@ -193,13 +195,19 @@
let selectedRegionId: string = ''
void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo?.filter((it) => it.name !== '') ?? []
regionInfo = _regionInfo ?? []
if (selectedRegionId === '' && regionInfo.length > 0) {
selectedRegionId = regionInfo[0].region
}
})
$: selectedRegionName = regionInfo.find((it) => it.region === selectedRegionId)?.name
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
$: selectedRegionName =
selectedRegionRef !== undefined
? selectedRegionRef.name.length > 0
? selectedRegionRef.name
: selectedRegionRef.region
: ''
$: byVersion = groupByArray(
workspaces.filter((it) => {
@@ -278,8 +286,11 @@
<ButtonMenu
selected={selectedRegionId}
autoSelectionIfOne
title={regionInfo.find((it) => it.region === selectedRegionId)?.name}
items={regionInfo.map((it) => ({ id: it.region === '' ? '#' : it.region, label: getEmbeddedLabel(it.name) }))}
title={selectedRegionName}
items={regionInfo.map((it) => ({
id: it.region === '' ? '#' : it.region,
label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
}))}
on:selected={(it) => {
selectedRegionId = it.detail === '#' ? '' : it.detail
}}
@@ -291,12 +302,13 @@
{#each Object.keys(dayRanges) as k}
{@const v = groupped.get(k) ?? []}
{@const hasMore = (groupped.get(k) ?? []).length > limit}
{@const activeV = v.filter((it) => it.mode === 'active' && (it.region ?? '') !== selectedRegionId)}
{@const archivedV = v.filter((it) => it.mode === 'archived')}
{@const deletedV = v.filter((it) => it.mode === 'deleted')}
{@const av = v.length - archivedV.length - deletedV.length}
{@const activeV = v.filter((it) => isActiveMode(it.mode) && it.region !== selectedRegionId)}
{@const activeAll = v.filter((it) => isActiveMode(it.mode))}
{@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
{@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
{@const maintenance = v.length - activeAll.length - archivedV.length - deletedV.length}
{#if v.length > 0}
<Expandable expandable={true} bordered={true}>
<Expandable expandable={true} bordered={true} expanded={search.trim().length > 0}>
<svelte:fragment slot="title">
<span class="fs-title focused-button flex-row-center">
{k} -
@@ -305,8 +317,8 @@
{:else}
{v.length}
{/if}
{#if av > 0}
- maitenance: {av}
{#if maintenance > 0}
- maitenance: {maintenance}
{/if}
</span>
</svelte:fragment>
@@ -324,18 +336,18 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="tools">
{#if archivedV.length > 0}
{#if activeAll.length > 0}
<Button
icon={IconStop}
label={getEmbeddedLabel(`Mass Archive ${archivedV.length}`)}
label={getEmbeddedLabel(`Mass Archive ${activeAll.length}`)}
kind={'ghost'}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Archive ${archivedV.length}`),
message: getEmbeddedLabel(`Please confirm archive ${archivedV.length} workspaces`),
label: getEmbeddedLabel(`Mass Archive ${activeAll.length}`),
message: getEmbeddedLabel(`Please confirm archive ${activeAll.length} workspaces`),
action: async () => {
void performWorkspaceOperation(
archivedV.map((it) => it.uuid),
activeAll.map((it) => it.uuid),
'archive'
)
}
@@ -351,8 +363,8 @@
label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Mass Migrate ${archivedV.length}`),
message: getEmbeddedLabel(`Please confirm migrate ${archivedV.length} workspaces`),
label: getEmbeddedLabel(`Mass Migrate ${activeV.length}`),
message: getEmbeddedLabel(`Please confirm migrate ${activeV.length} workspaces`),
action: async () => {
await performWorkspaceOperation(
activeV.map((it) => it.uuid),
@@ -371,38 +383,36 @@
{@const bIdx = backupIdx.get(workspace.uuid)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex fs-title cursor-pointer focused-button bordered">
<div class="flex p-2">
<span class="label overflow-label flex-row-center" style:width={'12rem'}>
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.uuid}`}>
<Grid column={10} equalHeight={false} columnGap={0}>
<span class="label overflow-label flex-row-center flex-grow flex-shrink">
{wsName}
<div class="ml-1">
<Button icon={IconOpen} size={'small'} on:click={() => select(workspace.url)} />
</div>
</span>
<div class="ml-1" style:width={'12rem'}>
{workspace.createdBy}
<div class="ml-1">
<!-- Retrieve created by-->
<!-- {workspace.createdBy} -->
</div>
<span class="label overflow-label" style:width={'8rem'}>
<span class="label overflow-label flex-shrink p-1">
{workspace.region ?? ''}
</span>
<span class="label overflow-label" style:width={'5rem'}>
<span class="label overflow-label flex-shrink p-1">
{lastUsageDays} days
</span>
<span class="label overflow-label" style:width={'10rem'}>
<span class="label overflow-label p-1">
{workspace.mode ?? '-'}
</span>
<span class="label overflow-label" style:width={'2rem'}>
{workspace.attempts}
<span class="label overflow-label flex-shrink">
{workspace.processingAttempts}
</span>
<span class="flex flex-between" style:width={'5rem'}>
<span class="flex flex-shrink">
{#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
({workspace.processingProgress}%)
{/if}
</span>
<span class="flex flex-between" style:width={'5rem'}>
<span class="flex flex-shrink">
{#if workspace.backupInfo != null}
{@const sz = Math.max(
workspace.backupInfo.backupSize,
@@ -419,7 +429,7 @@
[#{bIdx}]
{/if}
</span>
<span class="flex flex-between" style:width={'5rem'}>
<span class="flex flex-shrink">
{#if workspace.backupInfo != null}
{@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))}
@@ -430,82 +440,81 @@
{/if}
{/if}
</span>
</div>
<div class="flex flex-grow gap-1-5 flex-between">
<div class="flex flex-row-center gap-1-5">
{#if workspace.mode === 'active'}
<Button
icon={IconStop}
size={'small'}
label={getEmbeddedLabel('Archive')}
kind={'ghost'}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Archive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'archive')
}
})
}}
/>
{/if}
<div class="flex flex-grow gap-1-5 flex-shrink">
<div class="flex flex-row-center gap-1-5">
{#if workspace.mode === 'active'}
<Button
icon={IconStop}
size={'small'}
label={getEmbeddedLabel('Archive')}
kind={'ghost'}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Archive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'archive')
}
})
}}
/>
{/if}
{#if workspace.mode === 'archived'}
<Button
icon={IconStart}
size={'small'}
kind={'ghost'}
label={getEmbeddedLabel('Unarchive')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'unarchive')
}
})
}}
/>
{/if}
{#if workspace.mode === 'archived'}
<Button
icon={IconStart}
size={'small'}
kind={'ghost'}
label={getEmbeddedLabel('Unarchive')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'unarchive')
}
})
}}
/>
{/if}
{#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
<Button
icon={IconArrowRight}
size={'small'}
kind={'positive'}
label={getEmbeddedLabel('Migrate')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Migrate ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
}
})
}}
/>
{/if}
{#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
<Button
icon={IconArrowRight}
size={'small'}
kind={'positive'}
label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Migrate ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
}
})
}}
/>
{/if}
{#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
<Button
icon={IconStop}
size={'small'}
kind={'dangerous'}
label={getEmbeddedLabel('Delete')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Delete ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'delete')
}
})
}}
/>
{/if}
{#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
<Button
icon={IconStop}
size={'small'}
kind={'dangerous'}
label={getEmbeddedLabel('Delete')}
on:click={() => {
showPopup(MessageBox, {
label: getEmbeddedLabel(`Delete ${workspace.url}`),
message: getEmbeddedLabel('Please confirm'),
action: async () => {
await performWorkspaceOperation(workspace.uuid, 'delete')
}
})
}}
/>
{/if}
</div>
</div>
</div>
</Grid>
</div>
{/each}
</Expandable>
+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'
})
}}
@@ -243,6 +243,17 @@ export async function connect (title: string): Promise<Client | undefined> {
onArchived: () => {
translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
location.reload()
}, 5000)
})
},
onMigration: () => {
translateCB(plugin.string.WorkspaceIsMigrating, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
location.reload()
}, 5000)
})
},
// We need to refresh all active live queries and clear old queries.
+2 -1
View File
@@ -256,7 +256,8 @@ export default plugin(workbenchId, {
OpenInSidebar: '' as IntlString,
OpenInSidebarNewTab: '' as IntlString,
ConfigureWidgets: '' as IntlString,
WorkspaceIsArchived: '' as IntlString
WorkspaceIsArchived: '' as IntlString,
WorkspaceIsMigrating: '' as IntlString
},
icon: {
Search: '' as Asset
+5
View File
@@ -1140,6 +1140,11 @@
"projectFolder": "tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/tests-ws-sanity",
"projectFolder": "ws-tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/qms-tests-sanity",
"projectFolder": "qms-tests/sanity",
-1
View File
@@ -241,7 +241,6 @@ export class WorkspaceStatusMongoDbCollection implements DbCollection<WorkspaceS
for (const key of Object.keys(query)) {
const qVal = (query as any)[key]
if (key === 'workspaceUuid') {
res.uuid = qVal
} else {
+44 -44
View File
@@ -15,22 +15,22 @@
import { Analytics } from '@hcengineering/analytics'
import {
AccountRole,
concatLink,
Data,
isActiveMode,
isWorkspaceCreating,
MeasureContext,
SocialIdType,
systemAccountUuid,
Version,
SocialIdType,
type BackupStatus,
type Branding,
AccountRole,
type Person,
type WorkspaceMemberInfo,
type PersonUuid,
isActiveMode,
type WorkspaceUuid,
type WorkspaceMode
type WorkspaceMemberInfo,
type WorkspaceMode,
type WorkspaceUuid
} from '@hcengineering/core'
import platform, {
getMetadata,
@@ -45,48 +45,48 @@ import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
import { accountPlugin } from './plugin'
import type {
AccountDB,
AccountMethodHandler,
LoginInfo,
OtpInfo,
RegionInfo,
WorkspaceEvent,
WorkspaceOperation,
AccountMethodHandler,
WorkspaceLoginInfo,
WorkspaceInfoWithStatus,
WorkspaceStatus,
SocialId,
Workspace
Workspace,
WorkspaceEvent,
WorkspaceInfoWithStatus,
WorkspaceLoginInfo,
WorkspaceOperation,
WorkspaceStatus
} from './types'
import {
checkInvite,
cleanEmail,
EndpointKind,
getEndpoint,
getRegions,
verifyPassword,
isOtpValid,
confirmEmail,
createAccount,
createWorkspaceRecord,
doJoinByInvite,
EndpointKind,
getAccount,
getEmailSocialId,
getEndpoint,
getFrontUrl,
getRegions,
getRolePower,
getSesUrl,
getSocialIdByKey,
getWorkspaceById,
getWorkspaceInfoWithStatusById,
getWorkspaceInvite,
getWorkspaces,
getWorkspacesInfoWithStatusByIds,
GUEST_ACCOUNT,
isOtpValid,
selectWorkspace,
sendEmailConfirmation,
sendOtp,
setPassword,
wrap,
createWorkspaceRecord,
checkInvite,
GUEST_ACCOUNT,
getWorkspaceById,
getWorkspaceInvite,
getRolePower,
sendEmailConfirmation,
getEmailSocialId,
confirmEmail,
getAccount,
getSesUrl,
getFrontUrl,
getWorkspaceInfoWithStatusById,
signUpByEmail,
selectWorkspace,
doJoinByInvite,
getWorkspacesInfoWithStatusByIds,
getSocialIdByKey,
getWorkspaces
verifyPassword,
wrap
} from './utils'
// Move to config?
@@ -857,7 +857,7 @@ export async function performWorkspaceOperation (
): Promise<boolean> {
const { extra } = decodeTokenVerbose(ctx, token)
if (extra?.service !== 'admin') {
if (extra?.admin !== 'true') {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
@@ -874,7 +874,7 @@ export async function performWorkspaceOperation (
switch (event) {
case 'delete':
if (workspace.status.mode !== 'active') {
throw new PlatformError(unknownError('Archive allowed only for active workspaces'))
throw new PlatformError(unknownError('Delete allowed only for active workspaces'))
}
update.mode = 'pending-deletion'
@@ -883,7 +883,7 @@ export async function performWorkspaceOperation (
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
break
case 'archive':
if (isActiveMode(workspace.status.mode)) {
if (!isActiveMode(workspace.status.mode)) {
throw new PlatformError(unknownError('Archiving allowed only for active workspaces'))
}
@@ -921,7 +921,7 @@ export async function performWorkspaceOperation (
update.mode = 'migration-pending-backup'
// NOTE: will only work for Mongo accounts
;(update as any).targetRegion = params[0]
update.targetRegion = params[0]
update.processingAttempts = 0
update.processingProgress = 0
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
@@ -1278,7 +1278,7 @@ export async function updateWorkspaceInfo (
update.processingProgress = progress
break
case 'migrate-clean-done':
wsUpdate.region = (workspace as any).targetRegion ?? ''
wsUpdate.region = workspace.status.targetRegion ?? ''
update.mode = 'pending-restore'
update.processingProgress = progress
update.lastProcessingTime = Date.now() - processingTimeoutMs // To not wait for next step
@@ -1323,8 +1323,8 @@ export async function updateWorkspaceInfo (
await db.workspaceStatus.updateOne(
{ workspaceUuid: workspace.uuid },
{
...update,
lastProcessingTime: Date.now()
lastProcessingTime: Date.now(), // Some operations override it.
...update
}
)
+2
View File
@@ -87,6 +87,8 @@ export interface WorkspaceStatus {
processingAttempts?: number
processingMessage?: string
backupInfo?: BackupStatus
targetRegion?: string
}
export interface Workspace {
-15
View File
@@ -27,8 +27,6 @@ import {
systemAccountUuid,
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
isActiveMode,
isArchivingMode,
isMigrationMode,
type PersonUuid
} from '@hcengineering/core'
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
@@ -560,19 +558,6 @@ export async function selectWorkspace (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUrl }))
}
if (isArchivingMode(wsStatus.mode) || isMigrationMode(wsStatus.mode)) {
const result: WorkspaceLoginInfo = {
account: accountUuid,
endpoint: '',
token: generateToken(accountUuid, workspace.uuid),
workspace: workspace.uuid,
workspaceUrl: workspace.url,
workspaceDataId: workspace.dataId,
role
}
return result
}
}
return {
-4
View File
@@ -92,8 +92,6 @@ export async function backupWorkspace (
externalStorage: StorageAdapter
) => DbConfiguration,
region: string,
freshBackup: boolean = false,
clean: boolean = false,
downloadLimit: number,
contextVars: Record<string, any>,
@@ -129,8 +127,6 @@ export async function backupWorkspace (
return getConfig(ctx, mainDbUrl, workspace, branding, externalStorage)
},
region,
freshBackup,
clean,
downloadLimit,
[],
contextVars
+4 -39
View File
@@ -672,8 +672,6 @@ export async function backup (
include?: Set<string>
skipDomains: string[]
force: boolean
freshBackup: boolean // If passed as true, will download all documents except blobs as new backup
clean: boolean // If set will perform a clena of old backup files
timeout: number
connectTimeout: number
skipBlobContentTypes: string[]
@@ -687,8 +685,6 @@ export async function backup (
token?: string
} = {
force: false,
freshBackup: false,
clean: false,
timeout: 0,
skipDomains: [],
connectTimeout: 30000,
@@ -706,7 +702,6 @@ export async function backup (
ctx = ctx.newChild('backup', {
workspaceId,
force: options.force,
recheck: options.freshBackup,
timeout: options.timeout
})
@@ -767,7 +762,7 @@ export async function backup (
let lastTxChecked = false
// Skip backup if there is no transaction changes.
if (options.getLastTx !== undefined && !options.freshBackup) {
if (options.getLastTx !== undefined) {
lastTx = await options.getLastTx()
if (lastTx !== undefined) {
if (lastTx._id === backupInfo.lastTxId && !options.force) {
@@ -793,7 +788,7 @@ export async function backup (
? await options.getConnection()
: ((await createClient(transactorUrl, token, undefined, options.connectTimeout)) as CoreClient & BackupClient)
if (!lastTxChecked && !options.freshBackup) {
if (!lastTxChecked) {
lastTx = await connection.findOne(
core.class.Tx,
{ objectSpace: { $ne: core.space.Model } },
@@ -932,7 +927,7 @@ export async function backup (
if (digest.delete(id as Ref<Doc>)) {
oldHash.set(id as Ref<Doc>, currentHash)
}
if (currentHash !== serverDocHash || (options.freshBackup && domain !== DOMAIN_BLOB)) {
if (currentHash !== serverDocHash) {
if (changes.updated.has(id as Ref<Doc>)) {
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
}
@@ -1355,36 +1350,6 @@ export async function backup (
if (!canceled()) {
backupInfo.lastTxId = lastTx?._id ?? '0' // We could store last tx, since full backup is complete
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
if (options.freshBackup && options.clean) {
// Preparing a list of files to clean
ctx.info('Cleaning old backup files...')
for (const sn of backupInfo.snapshots.slice(0, backupInfo.snapshots.length - 1)) {
const filesToDelete: string[] = []
for (const [domain, dsn] of [...Object.entries(sn.domains)]) {
if (domain === DOMAIN_BLOB) {
continue
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (sn.domains as any)[domain]
filesToDelete.push(...(dsn.snapshots ?? []))
filesToDelete.push(...(dsn.storage ?? []))
if (dsn.snapshot !== undefined) {
filesToDelete.push(dsn.snapshot)
}
}
for (const file of filesToDelete) {
ctx.info('Removing file...', { file })
await storage.delete(file)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete sizeInfo[file]
}
}
ctx.info('Cleaning complete...')
await storage.writeFile(infoFile, gzipSync(JSON.stringify(backupInfo, undefined, 2), { level: defaultLevel }))
}
}
const addFileSize = async (file: string | undefined | null): Promise<void> => {
@@ -1735,7 +1700,7 @@ export async function restore (
try {
let serverEndpoint = await getTransactorEndpoint(token, 'external')
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${workspaceId}`, {
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
-8
View File
@@ -73,8 +73,6 @@ class BackupWorker {
) => DbConfiguration,
readonly region: string,
readonly contextVars: Record<string, any>,
readonly freshWorkspace: boolean = false,
readonly clean: boolean = false,
readonly skipDomains: string[] = []
) {}
@@ -282,8 +280,6 @@ class BackupWorker {
backup(ctx, '', wsIds, storage, {
skipDomains: this.skipDomains,
force: true,
freshBackup: this.freshWorkspace,
clean: this.clean,
timeout: this.config.Timeout * 1000,
connectTimeout: 5 * 60 * 1000, // 5 minutes to,
blobDownloadLimit: this.downloadLimit,
@@ -408,8 +404,6 @@ export async function doBackupWorkspace (
externalStorage: StorageAdapter
) => DbConfiguration,
region: string,
freshWorkspace: boolean,
clean: boolean,
downloadLimit: number,
skipDomains: string[],
contextVars: Record<string, any>,
@@ -423,8 +417,6 @@ export async function doBackupWorkspace (
getConfig,
region,
contextVars,
freshWorkspace,
clean,
skipDomains
)
backupWorker.downloadLimit = downloadLimit
+6 -6
View File
@@ -15,7 +15,6 @@
import {
type Account,
type PersonId,
type Branding,
type Class,
type Doc,
@@ -29,6 +28,8 @@ import {
type MeasureContext,
type ModelDb,
type Obj,
type PersonId,
type PersonUuid,
type Ref,
type SearchOptions,
type SearchQuery,
@@ -39,10 +40,9 @@ import {
type Tx,
type TxFactory,
type TxResult,
type WorkspaceUuid,
type WorkspaceIds,
type WorkspaceDataId,
type PersonUuid
type WorkspaceIds,
type WorkspaceUuid
} from '@hcengineering/core'
import type { Asset, Resource } from '@hcengineering/platform'
import type { LiveQuery } from '@hcengineering/query'
@@ -649,8 +649,8 @@ export interface AddSessionActive {
}
export type AddSessionResponse =
| AddSessionActive
| { upgrade: true }
| { error: any, terminate?: boolean, archived?: boolean }
| { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, specialError?: 'archived' | 'migration' }
export type SessionFactory = (token: Token, workspace: Workspace, account: Account) => Session
+8 -8
View File
@@ -54,6 +54,7 @@ import {
SessionManager,
StorageAdapter,
type SessionFactory,
type AddSessionResponse,
type ClientSessionCtx,
type ConnectionSocket,
type Session,
@@ -355,11 +356,7 @@ class TSessionManager implements SessionManager {
rawToken: string,
pipelineFactory: PipelineFactory,
sessionId: string | undefined
): Promise<
| { session: Session, context: MeasureContext, workspaceId: WorkspaceUuid }
| { upgrade: true, progress?: number }
| { error: any, terminate?: boolean, archived?: boolean }
> {
): Promise<AddSessionResponse> {
const { workspace: workspaceUuid } = token
let workspaceInfo: WorkspaceInfoWithStatus | undefined
@@ -376,15 +373,15 @@ class TSessionManager implements SessionManager {
if (isArchivingMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is archived'), terminate: true, archived: true }
return { error: new Error('Workspace is archived'), terminate: true, specialError: 'archived' }
}
if (isMigrationMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is in region migration'), terminate: true, archived: false }
return { error: new Error('Workspace is in region migration'), terminate: true, specialError: 'migration' }
}
if (isRestoringMode(workspaceInfo.mode)) {
// No access to disabled workspaces for regular users
return { error: new Error('Workspace is in backup restore'), terminate: true, archived: false }
return { error: new Error('Workspace is in backup restore'), terminate: true, specialError: 'migration' }
}
if (workspaceInfo.isDisabled === true && token.account !== systemAccountUuid && token.extra?.admin !== 'true') {
@@ -836,11 +833,14 @@ class TSessionManager implements SessionManager {
async forceClose (wsId: WorkspaceUuid, ignoreSocket?: ConnectionSocket): Promise<void> {
const ws = this.workspaces.get(wsId)
if (ws !== undefined) {
this.ctx.warn('force-close', { name: ws.workspaceName })
ws.upgrade = true // We need to similare upgrade to refresh all clients.
ws.closing = this.closeAll(wsId, ws, 99, 'force-close', ignoreSocket)
this.workspaces.delete(wsId)
await ws.closing
ws.closing = undefined
} else {
this.ctx.warn('force-close-unknown', { wsId })
}
}
+1 -1
View File
@@ -362,7 +362,7 @@ export async function upgradeModel (
const token = generateToken(systemAccountUuid, wsIds.uuid, { service: 'tool', admin: 'true' })
try {
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${wsIds.uuid}`, {
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
+34 -12
View File
@@ -12,23 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getClient as getAccountClient } from '@hcengineering/account-client'
import {
getBranding,
isArchivingMode,
isMigrationMode,
isRestoringMode,
systemAccountUuid,
type BrandingMap,
type Data,
type MeasureContext,
type Tx,
type Version,
type WorkspaceUpdateEvent,
isArchivingMode,
isMigrationMode,
isRestoringMode,
type WorkspaceInfoWithStatus,
getBranding,
systemAccountUuid
type WorkspaceUpdateEvent,
type WorkspaceUuid
} from '@hcengineering/core'
import { type MigrateOperation, type ModelLogger } from '@hcengineering/model'
import { getClient as getAccountClient } from '@hcengineering/account-client'
import { withRetryConnUntilSuccess, withRetryConnUntilTimeout } from '@hcengineering/server-client'
import {
getTransactorEndpoint,
withRetryConnUntilSuccess,
withRetryConnUntilTimeout
} from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { FileModelLogger, prepareTools } from '@hcengineering/server-tool'
import path from 'path'
@@ -420,6 +425,20 @@ export class WorkspaceWorker {
}
}
async sendTransactorMaitenance (token: string, ws: WorkspaceUuid): Promise<void> {
try {
let serverEndpoint = await getTransactorEndpoint(token)
serverEndpoint = serverEndpoint.replaceAll('wss://', 'https://').replace('ws://', 'http://')
console.log('sending event', serverEndpoint, ws)
await fetch(serverEndpoint + `/api/v1/manage?token=${token}&operation=force-close`, {
method: 'PUT'
})
} catch (err: any) {
console.log(err)
// Ignore
}
}
private async doWorkspaceOperation (
ctx: MeasureContext,
workspace: WorkspaceInfoWithStatus,
@@ -454,6 +473,8 @@ export class WorkspaceWorker {
case 'archiving-pending-backup':
case 'archiving-backup': {
await sendEvent('archiving-backup-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
if (await this.doBackup(ctx, workspace, opt, true)) {
await sendEvent('archiving-backup-done', 100)
}
@@ -476,6 +497,7 @@ export class WorkspaceWorker {
case 'deleting': {
// We should remove DB, not storages.
await sendEvent('delete-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
try {
await this.doCleanup(ctx, workspace, true)
} catch (err: any) {
@@ -489,6 +511,7 @@ export class WorkspaceWorker {
case 'migration-pending-backup':
case 'migration-backup':
await sendEvent('migrate-backup-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
if (await this.doBackup(ctx, workspace, opt, false)) {
await sendEvent('migrate-backup-done', 100)
}
@@ -497,6 +520,7 @@ export class WorkspaceWorker {
case 'migration-clean': {
// We should remove DB, not storages.
await sendEvent('migrate-clean-started', 0)
await this.sendTransactorMaitenance(token, workspace.uuid)
try {
await this.doCleanup(ctx, workspace, false)
} catch (err: any) {
@@ -590,8 +614,6 @@ export class WorkspaceWorker {
})
},
this.region,
archive,
archive,
50000,
['blob'],
sharedPipelineContextVars,
@@ -604,7 +626,7 @@ export class WorkspaceWorker {
}
)
if (result) {
console.log('backup completed')
ctx.info('backup completed')
return true
}
} finally {
@@ -681,7 +703,7 @@ export class WorkspaceWorker {
}
)
if (result) {
console.log('backup completed')
ctx.info('restore completed')
return true
}
} finally {
+15 -2
View File
@@ -225,7 +225,7 @@ export function startHttpServer (
}
case 'force-close': {
const wsId = req.query.wsId as WorkspaceUuid
void sessions.forceClose(wsId)
void sessions.forceClose(wsId ?? payload.workspace)
res.writeHead(200)
res.end()
return
@@ -433,7 +433,7 @@ export function startHttpServer (
if (webSocketData.session instanceof Promise) {
void webSocketData.session.then((s) => {
if ('error' in s) {
if (s.archived === true) {
if (s.specialError === 'archived') {
cs.send(
ctx,
{
@@ -446,6 +446,19 @@ export function startHttpServer (
false,
false
)
} else if (s.specialError === 'migration') {
cs.send(
ctx,
{
id: -1,
error: new Status(Severity.ERROR, platform.status.WorkspaceMigration, {
workspaceUuid: token.workspace
}),
terminate: s.terminate
},
false,
false
)
} else {
cs.send(
ctx,
+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
@@ -31,6 +31,11 @@ export class LoginPage {
await (await this.page.goto(`${PlatformURI}/login/login`))?.finished()
}
// ACTIONS
async gotoAdmin (): Promise<void> {
await (await this.page.goto(`${PlatformURI}/login/admin`))?.finished()
}
async clickSignUp (): Promise<void> {
await this.linkSignUp().click()
}
+1 -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 { DefaultWorkspace, generateId, PlatformURI, PlatformUser } from '../utils'
import { SelectWorkspacePage } from '../model/select-workspace-page'
@@ -10,8 +10,6 @@ import { IssuesPage } from '../model/tracker/issues-page'
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page'
import { SignInJoinPage } from '../model/signin-page'
import { UserProfilePage } from '../model/profile/user-profile-page'
import { faker } from '@faker-js/faker'
test.describe('Workspace tests', () => {
let loginPage: LoginPage
@@ -20,7 +18,6 @@ test.describe('Workspace tests', () => {
let leftSideMenuPage: LeftSideMenuPage
let trackerNavigationMenuPage: TrackerNavigationMenuPage
let issuesPage: IssuesPage
let userProfilePage: UserProfilePage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page)
@@ -29,7 +26,6 @@ test.describe('Workspace tests', () => {
leftSideMenuPage = new LeftSideMenuPage(page)
trackerNavigationMenuPage = new TrackerNavigationMenuPage(page)
issuesPage = new IssuesPage(page)
userProfilePage = new UserProfilePage(page)
})
test('Create a workspace with a custom name', async () => {
@@ -232,26 +228,4 @@ test.describe('Workspace tests', () => {
}
})
})
test('User can leave workspace', async () => {
const newUser: SignUpData = {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
password: '1234'
}
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await trackerNavigationMenuPage.checkIfTrackerSidebarIsVisible()
await userProfilePage.openProfileMenu()
await userProfilePage.selectProfileByName(newUser.lastName + ' ' + newUser.firstName)
await userProfilePage.clickLeaveWorkspaceButton()
await userProfilePage.clickLeaveWorkspaceCancelButton()
await userProfilePage.clickLeaveWorkspaceButton()
await userProfilePage.clickLeaveWorkspaceConfirmButton()
await expect(selectWorkspacePage.title()).toBeVisible()
})
})
+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