diff --git a/packages/ui/src/components/Loading.svelte b/packages/ui/src/components/Loading.svelte index 3f74bc74bc7..734e4b49766 100644 --- a/packages/ui/src/components/Loading.svelte +++ b/packages/ui/src/components/Loading.svelte @@ -35,8 +35,9 @@
-
+
+
diff --git a/plugins/login-resources/src/components/SelectWorkspace.svelte b/plugins/login-resources/src/components/SelectWorkspace.svelte index 123497e0e1c..d585453da9d 100644 --- a/plugins/login-resources/src/components/SelectWorkspace.svelte +++ b/plugins/login-resources/src/components/SelectWorkspace.svelte @@ -124,6 +124,9 @@
{wsName} + {#if workspace.creating === true} + ({workspace.createProgress}%) + {/if} {#if isAdmin && wsName !== workspace.workspace} diff --git a/plugins/login-resources/src/index.ts b/plugins/login-resources/src/index.ts index e893a41e21f..6c2895cef6f 100644 --- a/plugins/login-resources/src/index.ts +++ b/plugins/login-resources/src/index.ts @@ -17,7 +17,15 @@ import { type IntlString } from '@hcengineering/platform' import InviteLink from './components/InviteLink.svelte' import LoginApp from './components/LoginApp.svelte' -import { changePassword, getWorkspaces, leaveWorkspace, selectWorkspace, sendInvite, getEnpoint } from './utils' +import { + changePassword, + getWorkspaces, + leaveWorkspace, + selectWorkspace, + sendInvite, + getEnpoint, + fetchWorkspace +} from './utils' /*! * Anticrm Platform™ Login Plugin * © 2020, 2021 Anticrm Platform Contributors. @@ -34,6 +42,7 @@ export default async () => ({ LeaveWorkspace: leaveWorkspace, ChangePassword: changePassword, SelectWorkspace: selectWorkspace, + FetchWorkspace: fetchWorkspace, GetWorkspaces: getWorkspaces, SendInvite: sendInvite, GetEndpoint: getEnpoint diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index 8d79442168a..78503e1f12e 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -348,6 +348,55 @@ export async function selectWorkspace (workspace: string): Promise<[Status, Work } } +export async function fetchWorkspace (workspace: string): Promise<[Status, WorkspaceLoginInfo | undefined]> { + const accountsUrl = getMetadata(login.metadata.AccountsUrl) + + if (accountsUrl === undefined) { + throw new Error('accounts url not specified') + } + + const overrideToken = getMetadata(login.metadata.OverrideLoginToken) + const email = fetchMetadataLocalStorage(login.metadata.LoginEmail) ?? '' + if (overrideToken !== undefined) { + const endpoint = getMetadata(login.metadata.OverrideEndpoint) + if (endpoint !== undefined) { + return [OK, { token: overrideToken, endpoint, email, workspace, confirmed: true }] + } + } + + const token = getMetadata(presentation.metadata.Token) + if (token === undefined) { + return [unknownStatus('Please login'), undefined] + } + + const request = { + method: 'getWorkspaceInfo', + params: [token] + } + + try { + const response = await fetch(accountsUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }) + const result = await response.json() + if (result.error == null) { + Analytics.handleEvent('Fetch workspace') + Analytics.setTag('workspace', workspace) + } else { + await handleStatusError('Fetch workspace error', result.error) + } + return [result.error ?? OK, result.result] + } catch (err: any) { + Analytics.handleError(err) + return [unknownError(err), undefined] + } +} + export function setLoginInfo (loginInfo: WorkspaceLoginInfo): void { const tokens: Record = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {} tokens[loginInfo.workspace] = loginInfo.token diff --git a/plugins/login/src/index.ts b/plugins/login/src/index.ts index 45863b04975..72c7dd88943 100644 --- a/plugins/login/src/index.ts +++ b/plugins/login/src/index.ts @@ -29,6 +29,9 @@ export interface Workspace { workspace: string // workspace Url workspaceName?: string // A company name workspaceId: string // A unique identifier for the workspace + + creating?: boolean + createProgress?: number } /** @@ -36,6 +39,8 @@ export interface Workspace { */ export interface WorkspaceLoginInfo extends LoginInfo { workspace: string + creating?: boolean + createProgress?: number } /** @@ -76,6 +81,7 @@ export default plugin(loginId, { LeaveWorkspace: '' as Resource<(email: string) => Promise>, ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise>, SelectWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>, + FetchWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>, GetWorkspaces: '' as Resource<() => Promise>, GetEndpoint: '' as Resource<() => Promise> } diff --git a/plugins/workbench-assets/lang/en.json b/plugins/workbench-assets/lang/en.json index d483a13f817..02abe025ccf 100644 --- a/plugins/workbench-assets/lang/en.json +++ b/plugins/workbench-assets/lang/en.json @@ -30,6 +30,7 @@ "PleaseUpdate": "Please update", "ServerUnderMaintenance": "Server is under maintenance", "MobileNotSupported": "Sorry, mobile devices support coming soon. In the meantime, please use Desktop", - "LogInAnyway": "Log in anyway" + "LogInAnyway": "Log in anyway", + "WorkspaceCreating": "Creation in progress..." } } diff --git a/plugins/workbench-assets/lang/es.json b/plugins/workbench-assets/lang/es.json index cf9e4d068db..3a65e606453 100644 --- a/plugins/workbench-assets/lang/es.json +++ b/plugins/workbench-assets/lang/es.json @@ -30,6 +30,7 @@ "PleaseUpdate": "Por favor, actualice", "ServerUnderMaintenance": "El servidor está en mantenimiento", "MobileNotSupported": "Disculpa, el soporte para dispositivos móviles estará disponible próximamente. Mientras tanto, por favor usa el escritorio.", - "LogInAnyway": "Iniciar sesión de todas formas" + "LogInAnyway": "Iniciar sesión de todas formas", + "WorkspaceCreating": "Creation in progress..." } } \ No newline at end of file diff --git a/plugins/workbench-assets/lang/pt.json b/plugins/workbench-assets/lang/pt.json index 9a4aabc56d2..51d7f8d127a 100644 --- a/plugins/workbench-assets/lang/pt.json +++ b/plugins/workbench-assets/lang/pt.json @@ -30,6 +30,7 @@ "PleaseUpdate": "Atualize", "ServerUnderMaintenance": "Servidor em manutenção", "MobileNotSupported": "Desculpe, o suporte para dispositivos móveis estará disponível em breve. Enquanto isso, por favor, use o Desktop.", - "LogInAnyway": "Entrar de qualquer maneira" + "LogInAnyway": "Entrar de qualquer maneira", + "WorkspaceCreating": "Creation in progress..." } } \ No newline at end of file diff --git a/plugins/workbench-assets/lang/ru.json b/plugins/workbench-assets/lang/ru.json index 0d2d57046c4..fd9728755ce 100644 --- a/plugins/workbench-assets/lang/ru.json +++ b/plugins/workbench-assets/lang/ru.json @@ -30,6 +30,7 @@ "PleaseUpdate": "Пожалуйста, обновите приложение", "ServerUnderMaintenance": "Обслуживание сервера", "MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.", - "LogInAnyway": "Все равно войти" + "LogInAnyway": "Все равно войти", + "WorkspaceCreating": "Пространство создается..." } } diff --git a/plugins/workbench-resources/src/components/WorkbenchApp.svelte b/plugins/workbench-resources/src/components/WorkbenchApp.svelte index a4caa38202e..02ea6ab250d 100644 --- a/plugins/workbench-resources/src/components/WorkbenchApp.svelte +++ b/plugins/workbench-resources/src/components/WorkbenchApp.svelte @@ -28,8 +28,9 @@ import { connect, disconnect, versionError } from '../connect' import { workbenchId } from '@hcengineering/workbench' - import workbench from '../plugin' import { onDestroy } from 'svelte' + import workbench from '../plugin' + import { workspaceCreating } from '../utils' const isNeedUpgrade = window.location.host === '' @@ -54,7 +55,14 @@ {:else} {#key $location.path[1]} {#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')} - + + {#if ($workspaceCreating ?? -1) > 0} +
+
+ {/if} +
{:then client} {#if !client && versionError}
diff --git a/plugins/workbench-resources/src/connect.ts b/plugins/workbench-resources/src/connect.ts index 276a466190b..91bf0e44aa3 100644 --- a/plugins/workbench-resources/src/connect.ts +++ b/plugins/workbench-resources/src/connect.ts @@ -23,6 +23,7 @@ import { setMetadataLocalStorage } from '@hcengineering/ui' import plugin from './plugin' +import { workspaceCreating } from './utils' export let versionError: string | undefined = '' @@ -63,6 +64,7 @@ export async function connect (title: string): Promise { } const tokens: Record = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {} let token = tokens[ws] + if (token === undefined && getMetadata(presentation.metadata.Token) !== undefined) { const selectWorkspace = await getResource(login.function.SelectWorkspace) const loginInfo = await ctx.with('select-workspace', {}, async () => (await selectWorkspace(ws))[1]) @@ -73,6 +75,22 @@ export async function connect (title: string): Promise { } } setMetadata(presentation.metadata.Token, token) + + const fetchWorkspace = await getResource(login.function.FetchWorkspace) + let loginInfo = await ctx.with('select-workspace', {}, async () => (await fetchWorkspace(ws))[1]) + if (loginInfo?.creating === true) { + while (true) { + workspaceCreating.set(loginInfo?.createProgress ?? 0) + loginInfo = await ctx.with('select-workspace', {}, async () => (await fetchWorkspace(ws))[1]) + workspaceCreating.set(loginInfo?.createProgress) + if (loginInfo?.creating === false) { + workspaceCreating.set(-1) + break + } + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } + document.cookie = encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/' diff --git a/plugins/workbench-resources/src/plugin.ts b/plugins/workbench-resources/src/plugin.ts index f709c287ebc..92c2e1c0b9c 100644 --- a/plugins/workbench-resources/src/plugin.ts +++ b/plugins/workbench-resources/src/plugin.ts @@ -43,7 +43,8 @@ export default mergeIds(workbenchId, workbench, { NewVersionAvailable: '' as IntlString, PleaseUpdate: '' as IntlString, MobileNotSupported: '' as IntlString, - LogInAnyway: '' as IntlString + LogInAnyway: '' as IntlString, + WorkspaceCreating: '' as IntlString }, metadata: { MobileAllowed: '' as Metadata diff --git a/plugins/workbench-resources/src/utils.ts b/plugins/workbench-resources/src/utils.ts index dcf82f0da5e..2d132644351 100644 --- a/plugins/workbench-resources/src/utils.ts +++ b/plugins/workbench-resources/src/utils.ts @@ -33,6 +33,8 @@ import view from '@hcengineering/view' import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench' import { writable } from 'svelte/store' +export const workspaceCreating = writable(undefined) + export function getSpecialSpaceClass (model: NavigatorModel): Array>> { const spaceResult = model.spaces.map((x) => x.spaceClass) const result = (model.specials ?? []) diff --git a/pods/account/src/index.ts b/pods/account/src/index.ts index 699d7142560..1e938c96831 100644 --- a/pods/account/src/index.ts +++ b/pods/account/src/index.ts @@ -14,7 +14,7 @@ // limitations under the License. // -import account, { ACCOUNT_DB, type AccountMethod, accountId } from '@hcengineering/account' +import account, { ACCOUNT_DB, type AccountMethod, accountId, cleanInProgressWorkspaces } from '@hcengineering/account' import accountEn from '@hcengineering/account/lang/en.json' import accountRu from '@hcengineering/account/lang/ru.json' import { registerProviders } from '@hcengineering/auth-providers' @@ -93,6 +93,9 @@ export function serveAccount (measureCtx: MeasureContext, methods: Record { const db = p.db(ACCOUNT_DB) registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL) + + // We need to clean workspace with creating === true, since server is restarted. + void cleanInProgressWorkspaces(db, productId) }) const extractToken = (header: IncomingHttpHeaders): string | undefined => { diff --git a/server/account/src/index.ts b/server/account/src/index.ts index eb4317f82a4..0e0b1e35c29 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -31,6 +31,7 @@ import core, { generateId, getWorkspaceId, MeasureContext, + MeasureMetricsContext, RateLimiter, Ref, systemAccountEmail, @@ -112,6 +113,9 @@ export interface Workspace { lastVisit: number createdBy: string + + creating?: boolean + createProgress?: number // Some progress } /** @@ -129,6 +133,9 @@ export interface LoginInfo { export interface WorkspaceLoginInfo extends LoginInfo { workspace: string productId: string + + creating?: boolean + createProgress?: number } /** @@ -344,12 +351,14 @@ export async function selectWorkspace ( email, token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)), workspace: workspaceUrl, - productId + productId, + creating: workspaceInfo.creating, + createProgress: workspaceInfo.createProgress } } if (workspaceInfo !== null) { - if (workspaceInfo.disabled === true) { + if (workspaceInfo.disabled === true && workspaceInfo.creating !== true) { await ctx.error('workspace disabled', { workspaceUrl, email }) throw new PlatformError( new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }) @@ -364,7 +373,9 @@ export async function selectWorkspace ( email, token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)), workspace: workspaceUrl, - productId + productId, + creating: workspaceInfo.creating, + createProgress: workspaceInfo.createProgress } return result } @@ -675,6 +686,19 @@ export async function setWorkspaceDisabled (db: Db, workspaceId: Workspace['_id' await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $set: { disabled } }) } +export async function cleanInProgressWorkspaces (db: Db, productId: string): Promise { + const toDelete = ( + await db + .collection(WORKSPACE_COLLECTION) + .find(withProductId(productId, { creating: true })) + .toArray() + ).map((it) => ({ ...it, productId })) + const ctx = new MeasureMetricsContext('clean', {}) + for (const d of toDelete) { + await dropWorkspace(ctx, db, productId, d.workspace) + } +} + /** * @public */ @@ -737,6 +761,8 @@ async function generateWorkspaceRecord ( workspaceName, accounts: [], disabled: true, + creating: true, + createProgress: 0, createdOn: Date.now(), lastVisit: Date.now(), createdBy: email @@ -767,6 +793,8 @@ async function generateWorkspaceRecord ( workspaceName, accounts: [], disabled: true, + creating: true, + createProgress: 0, createdOn: Date.now(), lastVisit: Date.now(), createdBy: email @@ -808,7 +836,8 @@ export async function createWorkspace ( productId: string, email: string, workspaceName: string, - workspace?: string + workspace?: string, + notifyHandler?: (workspace: Workspace) => void ): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client }> { return await rateLimiter.exec(async () => { // We need to search for duplicate workspaceUrl @@ -818,6 +847,18 @@ export async function createWorkspace ( searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace) const workspaceInfo = await searchPromise + + notifyHandler?.(workspaceInfo) + + const wsColl = db.collection>(WORKSPACE_COLLECTION) + + async function updateInfo (ops: Partial): Promise { + await wsColl.updateOne({ _id: workspaceInfo._id }, { $set: ops }) + console.log('update', ops) + } + + await updateInfo({ createProgress: 10 }) + let client: Client | undefined const childLogger = ctx.newChild( 'createWorkspace', @@ -838,24 +879,54 @@ export async function createWorkspace ( const wsId = getWorkspaceId(workspaceInfo.workspace, productId) if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) { // Just any valid model for transactor to be able to function - await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, true) + await ( + await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, async (value) => { + await updateInfo({ createProgress: Math.round((Math.min(value, 100) / 100) * 20) }) + }) + ).close() + await updateInfo({ createProgress: 20 }) // Clone init workspace. await cloneWorkspace( getTransactor(), getWorkspaceId(initWS, productId), - getWorkspaceId(workspaceInfo.workspace, productId) + getWorkspaceId(workspaceInfo.workspace, productId), + true, + async (value) => { + await updateInfo({ createProgress: 20 + Math.round((Math.min(value, 100) / 100) * 30) }) + } ) - client = await upgradeModel(ctx, getTransactor(), wsId, txes, migrationOperation, ctxModellogger) + await updateInfo({ createProgress: 50 }) + client = await upgradeModel( + ctx, + getTransactor(), + wsId, + txes, + migrationOperation, + ctxModellogger, + true, + async (value) => { + await updateInfo({ createProgress: Math.round(50 + (Math.min(value, 100) / 100) * 40) }) + } + ) + await updateInfo({ createProgress: 90 }) } else { - client = await initModel(ctx, getTransactor(), wsId, txes, migrationOperation, ctxModellogger) + client = await initModel( + ctx, + getTransactor(), + wsId, + txes, + migrationOperation, + ctxModellogger, + async (value) => { + await updateInfo({ createProgress: Math.round(Math.min(value, 100)) }) + } + ) } } catch (err: any) { return { workspaceInfo, err, client: null as any } } // Workspace is created, we need to clear disabled flag. - await db - .collection>(WORKSPACE_COLLECTION) - .updateOne({ _id: workspaceInfo._id }, { $set: { disabled: false } }) + await updateInfo({ createProgress: 100, disabled: false, creating: false }) return { workspaceInfo, client } }) } @@ -901,7 +972,16 @@ export async function upgradeWorkspace ( } ) await ( - await upgradeModel(ctx, getTransactor(), getWorkspaceId(ws.workspace, productId), txes, migrationOperation, logger) + await upgradeModel( + ctx, + getTransactor(), + getWorkspaceId(ws.workspace, productId), + txes, + migrationOperation, + logger, + false, + async (value) => {} + ) ).close() return versionStr } @@ -933,42 +1013,56 @@ export const createUserWorkspace = } } - const { workspaceInfo, err, client } = await createWorkspace( - ctx, - version, - txes, - migrationOperation, - db, - productId, - email, - workspaceName - ) + async function doCreate (info: Account, notifyHandler: (workspace: Workspace) => void): Promise { + const { workspaceInfo, err, client } = await createWorkspace( + ctx, + version, + txes, + migrationOperation, + db, + productId, + email, + workspaceName, + undefined, + notifyHandler + ) - if (err != null) { - await ctx.error('failed to create workspace', { err, workspaceName, email }) - // We need to drop workspace, to prevent wrong data usage. + if (err != null) { + await ctx.error('failed to create workspace', { err, workspaceName, email }) + // We need to drop workspace, to prevent wrong data usage. - await db.collection(WORKSPACE_COLLECTION).updateOne( - { - _id: workspaceInfo._id - }, - { $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } } - ) - throw err - } - try { - info.lastWorkspace = Date.now() - - // Update last workspace time. - await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } }) - - const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) - const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null - await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client) - await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client) - } finally { - await client?.close() + await db.collection(WORKSPACE_COLLECTION).updateOne( + { + _id: workspaceInfo._id + }, + { $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } } + ) + throw err + } + try { + info.lastWorkspace = Date.now() + + // Update last workspace time. + await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } }) + + const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) + const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null + await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client) + await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client) + await ctx.info('Creating server side done', { workspaceName, email }) + } finally { + await client?.close() + } } + + const workspaceInfo = await new Promise((resolve) => { + void doCreate(info, (info: Workspace) => { + resolve(info) + }) + }) + + await assignWorkspaceRaw(db, { account: info, workspace: workspaceInfo }) + const result = { endpoint: getEndpoint(), email, @@ -976,7 +1070,7 @@ export const createUserWorkspace = productId, workspace: workspaceInfo.workspaceUrl } - await ctx.info('Creating workspace done', { workspaceName, email }) + await ctx.info('Creating user side done', { workspaceName, email }) return result } @@ -1051,7 +1145,7 @@ export async function getUserWorkspaces ( .find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } })) .toArray() ) - .filter((it) => it.disabled !== true) + .filter((it) => it.disabled !== true || it.creating === true) .map(mapToClientWorkspace) } @@ -1094,7 +1188,7 @@ export async function getWorkspaceInfo ( const [ws] = ( await db.collection(WORKSPACE_COLLECTION).find(withProductId(productId, query)).toArray() - ).filter((it) => it.disabled !== true || account?.admin === true) + ).filter((it) => it.disabled !== true || account?.admin === true || it.creating === true) if (ws == null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) } @@ -1198,6 +1292,13 @@ export async function assignWorkspace ( } // Add account into workspace. + await assignWorkspaceRaw(db, workspaceInfo) + + await ctx.info('assign-workspace success', { email, workspaceId }) + return workspaceInfo.workspace +} + +async function assignWorkspaceRaw (db: Db, workspaceInfo: { account: Account, workspace: Workspace }): Promise { await db .collection(WORKSPACE_COLLECTION) .updateOne({ _id: workspaceInfo.workspace._id }, { $addToSet: { accounts: workspaceInfo.account._id } }) @@ -1206,9 +1307,6 @@ export async function assignWorkspace ( await db .collection(ACCOUNT_COLLECTION) .updateOne({ _id: workspaceInfo.account._id }, { $addToSet: { workspaces: workspaceInfo.workspace._id } }) - - await ctx.info('assign-workspace success', { email, workspaceId }) - return workspaceInfo.workspace } async function createEmployee (ops: TxOperations, name: string, _email: string): Promise> { diff --git a/server/backup/src/index.ts b/server/backup/src/index.ts index a34d044b1da..28c03c5c2ec 100644 --- a/server/backup/src/index.ts +++ b/server/backup/src/index.ts @@ -206,7 +206,8 @@ export async function cloneWorkspace ( transactorUrl: string, sourceWorkspaceId: WorkspaceId, targetWorkspaceId: WorkspaceId, - clearTime: boolean = true + clearTime: boolean = true, + progress: (value: number) => Promise ): Promise { const sourceConnection = (await connect(transactorUrl, sourceWorkspaceId, undefined, { mode: 'backup' @@ -220,6 +221,7 @@ export async function cloneWorkspace ( .domains() .filter((it) => it !== DOMAIN_TRANSIENT && it !== DOMAIN_MODEL) + let i = 0 for (const c of domains) { console.log('clone domain...', c) @@ -322,6 +324,9 @@ export async function cloneWorkspace ( continue } } + + i++ + await progress((100 / domains.length) * i) } } catch (err: any) { console.error(err) diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index f6bbb81d63e..c95a22534b8 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -112,8 +112,8 @@ export async function initModel ( rawTxes: Tx[], migrateOperations: [string, MigrateOperation][], logger: ModelLogger = consoleModelLogger, - skipOperations: boolean = false -): Promise { + progress: (value: number) => Promise +): Promise { const { mongodbUri, storageAdapter: minio, txes } = prepareTools(rawTxes) if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { throw Error('Model txes must target only core.space.Model') @@ -129,39 +129,48 @@ export async function initModel ( const result = await db.collection(DOMAIN_TX).insertMany(txes as Document[]) logger.log('model transactions inserted.', { count: result.insertedCount }) + await progress(10) + logger.log('creating data...', { transactorUrl }) const { model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId) + await progress(20) + logger.log('create minio bucket', { workspaceId }) if (!(await minio.exists(ctx, workspaceId))) { await minio.make(ctx, workspaceId) } - if (!skipOperations) { - connection = (await connect( - transactorUrl, - workspaceId, - undefined, - { - model: 'upgrade', - admin: 'true' - }, - model - )) as unknown as CoreClient & BackupClient + connection = (await connect( + transactorUrl, + workspaceId, + undefined, + { + model: 'upgrade', + admin: 'true' + }, + model + )) as unknown as CoreClient & BackupClient - try { - for (const op of migrateOperations) { - logger.log('Migrate', { name: op[0] }) - await op[1].upgrade(connection, logger) - } - - // Create update indexes - await createUpdateIndexes(ctx, connection, db, logger) - } catch (e: any) { - logger.error('error', { error: e }) - throw e + try { + let i = 0 + for (const op of migrateOperations) { + logger.log('Migrate', { name: op[0] }) + await op[1].upgrade(connection, logger) + i++ + await progress(20 + (((100 / migrateOperations.length) * i) / 100) * 10) } - return connection + await progress(30) + + // Create update indexes + await createUpdateIndexes(ctx, connection, db, logger, async (value) => { + await progress(30 + (Math.min(value, 100) / 100) * 70) + }) + await progress(100) + } catch (e: any) { + logger.error('error', { error: e }) + throw e } + return connection } finally { _client.close() } @@ -177,7 +186,8 @@ export async function upgradeModel ( rawTxes: Tx[], migrateOperations: [string, MigrateOperation][], logger: ModelLogger = consoleModelLogger, - skipTxUpdate: boolean = false + skipTxUpdate: boolean = false, + progress: (value: number) => Promise ): Promise { const { mongodbUri, txes } = prepareTools(rawTxes) @@ -193,6 +203,7 @@ export async function upgradeModel ( if (!skipTxUpdate) { logger.log('removing model...', { workspaceId: workspaceId.name }) + await progress(10) // we're preserving accounts (created by core.account.System). const result = await ctx.with( 'mongo-delete', @@ -214,16 +225,20 @@ export async function upgradeModel ( ) logger.log('model transactions inserted.', { workspaceId: workspaceId.name, count: insert.insertedCount }) + await progress(20) } const { hierarchy, modelDb, model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId) await ctx.with('migrate', {}, async () => { const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger) + let i = 0 for (const op of migrateOperations) { const t = Date.now() await op[1].migrate(migrateClient, logger) logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t }) + await progress(20 + ((100 / migrateOperations.length) * i * 20) / 100) + i++ } }) logger.log('Apply upgrade operations', { workspaceId: workspaceId.name }) @@ -245,16 +260,23 @@ export async function upgradeModel ( ) ) - // Create update indexes - await ctx.with('create-indexes', {}, async (ctx) => { - await createUpdateIndexes(ctx, connection, db, logger) - }) + if (!skipTxUpdate) { + // Create update indexes + await ctx.with('create-indexes', {}, async (ctx) => { + await createUpdateIndexes(ctx, connection, db, logger, async (value) => { + await progress(40 + (Math.min(value, 100) / 100) * 20) + }) + }) + } await ctx.with('upgrade', {}, async () => { + let i = 0 for (const op of migrateOperations) { const t = Date.now() await op[1].upgrade(connection, logger) logger.log('upgrade:', { operation: op[0], time: Date.now() - t, workspaceId: workspaceId.name }) + await progress(60 + ((100 / migrateOperations.length) * i * 40) / 100) + i++ } }) return connection @@ -295,7 +317,8 @@ async function createUpdateIndexes ( ctx: MeasureContext, connection: CoreClient, db: Db, - logger: ModelLogger + logger: ModelLogger, + progress: (value: number) => Promise ): Promise { const classes = await ctx.with('find-classes', {}, async () => await connection.findAll(core.class.Class, {})) @@ -339,6 +362,8 @@ async function createUpdateIndexes ( {}, async () => await db.listCollections({}, { nameOnly: true }).toArray() ) + const promises: Promise[] = [] + let completed = 0 for (const [d, v] of domains.entries()) { const collInfo = collections.find((it) => it.name === d) if (collInfo == null) { @@ -352,13 +377,25 @@ async function createUpdateIndexes ( const name = typeof vv === 'string' ? `${key}_1` : `${key}_${vv[key]}` const exists = await collection.indexExists(name) if (!exists) { - await collection.createIndex(vv) bb.push(vv) } } catch (err: any) { logger.error('error: failed to create index', { d, vv, err }) } } + for (const vv of bb) { + promises.push( + collection + .createIndex(vv, { + background: true + }) + .then(async () => { + completed++ + await progress((100 / bb.length) * completed) + }) + ) + } + await Promise.all(promises) if (bb.length > 0) { logger.log('created indexes', { d, bb }) } diff --git a/server/ws/src/server.ts b/server/ws/src/server.ts index 8356674838f..b038f14146b 100644 --- a/server/ws/src/server.ts +++ b/server/ws/src/server.ts @@ -17,6 +17,7 @@ import core, { TxFactory, WorkspaceEvent, generateId, + systemAccountEmail, toWorkspaceString, type MeasureContext, type Ref, @@ -183,6 +184,7 @@ class TSessionManager implements SessionManager { workspace: string workspaceUrl?: string | null workspaceName?: string + creating?: boolean }> { const userInfo = await ( await fetch(accounts, { @@ -219,6 +221,10 @@ class TSessionManager implements SessionManager { let workspaceInfo = await ctx.with('check-token', {}, async (ctx) => accountsUrl !== '' ? await this.getWorkspaceInfo(accountsUrl, rawToken) : this.wsFromToken(token) ) + if (workspaceInfo?.creating === true && token.email !== systemAccountEmail) { + // No access to workspace for token. + return { error: new Error(`Workspace during creation phase ${token.email} ${token.workspace.name}`) } + } if (workspaceInfo === undefined && token.extra?.admin !== 'true') { // No access to workspace for token. return { error: new Error(`No access to workspace for token ${token.email} ${token.workspace.name}`) } @@ -307,6 +313,7 @@ class TSessionManager implements SessionManager { workspace: string workspaceUrl?: string | null workspaceName?: string + creating?: boolean } { return { workspace: token.workspace.name,