UBERF-6469: Rework workspace creation to more informative (#5291)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-04-10 20:33:17 +07:00 committed by GitHub
parent d971c46b28
commit 9419278973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 344 additions and 93 deletions

View File

@ -35,8 +35,9 @@
</script> </script>
<div class="spinner-container" class:fullSize={!shrink}> <div class="spinner-container" class:fullSize={!shrink}>
<div data-label={label} class="inner" class:labeled={label !== ''}> <div data-label={label} class="inner flex-row-center" class:labeled={label !== ''}>
<Spinner {size} /> <Spinner {size} />
<slot />
</div> </div>
</div> </div>

View File

@ -124,6 +124,9 @@
<div class="flex flex-col flex-grow"> <div class="flex flex-col flex-grow">
<span class="label overflow-label flex-center"> <span class="label overflow-label flex-center">
{wsName} {wsName}
{#if workspace.creating === true}
({workspace.createProgress}%)
{/if}
</span> </span>
{#if isAdmin && wsName !== workspace.workspace} {#if isAdmin && wsName !== workspace.workspace}
<span class="text-xs flex-center"> <span class="text-xs flex-center">

View File

@ -17,7 +17,15 @@
import { type IntlString } from '@hcengineering/platform' import { type IntlString } from '@hcengineering/platform'
import InviteLink from './components/InviteLink.svelte' import InviteLink from './components/InviteLink.svelte'
import LoginApp from './components/LoginApp.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 * Anticrm Platform Login Plugin
* © 2020, 2021 Anticrm Platform Contributors. * © 2020, 2021 Anticrm Platform Contributors.
@ -34,6 +42,7 @@ export default async () => ({
LeaveWorkspace: leaveWorkspace, LeaveWorkspace: leaveWorkspace,
ChangePassword: changePassword, ChangePassword: changePassword,
SelectWorkspace: selectWorkspace, SelectWorkspace: selectWorkspace,
FetchWorkspace: fetchWorkspace,
GetWorkspaces: getWorkspaces, GetWorkspaces: getWorkspaces,
SendInvite: sendInvite, SendInvite: sendInvite,
GetEndpoint: getEnpoint GetEndpoint: getEnpoint

View File

@ -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 { export function setLoginInfo (loginInfo: WorkspaceLoginInfo): void {
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {} const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
tokens[loginInfo.workspace] = loginInfo.token tokens[loginInfo.workspace] = loginInfo.token

View File

@ -29,6 +29,9 @@ export interface Workspace {
workspace: string // workspace Url workspace: string // workspace Url
workspaceName?: string // A company name workspaceName?: string // A company name
workspaceId: string // A unique identifier for the workspace workspaceId: string // A unique identifier for the workspace
creating?: boolean
createProgress?: number
} }
/** /**
@ -36,6 +39,8 @@ export interface Workspace {
*/ */
export interface WorkspaceLoginInfo extends LoginInfo { export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string workspace: string
creating?: boolean
createProgress?: number
} }
/** /**
@ -76,6 +81,7 @@ export default plugin(loginId, {
LeaveWorkspace: '' as Resource<(email: string) => Promise<void>>, LeaveWorkspace: '' as Resource<(email: string) => Promise<void>>,
ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise<void>>, ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise<void>>,
SelectWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>, SelectWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
FetchWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
GetWorkspaces: '' as Resource<() => Promise<Workspace[]>>, GetWorkspaces: '' as Resource<() => Promise<Workspace[]>>,
GetEndpoint: '' as Resource<() => Promise<string>> GetEndpoint: '' as Resource<() => Promise<string>>
} }

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Please update", "PleaseUpdate": "Please update",
"ServerUnderMaintenance": "Server is under maintenance", "ServerUnderMaintenance": "Server is under maintenance",
"MobileNotSupported": "Sorry, mobile devices support coming soon. In the meantime, please use Desktop", "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..."
} }
} }

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Por favor, actualice", "PleaseUpdate": "Por favor, actualice",
"ServerUnderMaintenance": "El servidor está en mantenimiento", "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.", "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..."
} }
} }

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Atualize", "PleaseUpdate": "Atualize",
"ServerUnderMaintenance": "Servidor em manutenção", "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.", "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..."
} }
} }

View File

@ -30,6 +30,7 @@
"PleaseUpdate": "Пожалуйста, обновите приложение", "PleaseUpdate": "Пожалуйста, обновите приложение",
"ServerUnderMaintenance": "Обслуживание сервера", "ServerUnderMaintenance": "Обслуживание сервера",
"MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.", "MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.",
"LogInAnyway": "Все равно войти" "LogInAnyway": "Все равно войти",
"WorkspaceCreating": "Пространство создается..."
} }
} }

View File

@ -28,8 +28,9 @@
import { connect, disconnect, versionError } from '../connect' import { connect, disconnect, versionError } from '../connect'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import workbench from '../plugin'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import workbench from '../plugin'
import { workspaceCreating } from '../utils'
const isNeedUpgrade = window.location.host === '' const isNeedUpgrade = window.location.host === ''
@ -54,7 +55,14 @@
{:else} {:else}
{#key $location.path[1]} {#key $location.path[1]}
{#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')} {#await connect(getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform')}
<Loading /> <Loading>
{#if ($workspaceCreating ?? -1) > 0}
<div class="ml-1">
<Label label={workbench.string.WorkspaceCreating} />
{$workspaceCreating} %
</div>
{/if}
</Loading>
{:then client} {:then client}
{#if !client && versionError} {#if !client && versionError}
<div class="version-wrapper"> <div class="version-wrapper">

View File

@ -23,6 +23,7 @@ import {
setMetadataLocalStorage setMetadataLocalStorage
} from '@hcengineering/ui' } from '@hcengineering/ui'
import plugin from './plugin' import plugin from './plugin'
import { workspaceCreating } from './utils'
export let versionError: string | undefined = '' export let versionError: string | undefined = ''
@ -63,6 +64,7 @@ export async function connect (title: string): Promise<Client | undefined> {
} }
const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {} const tokens: Record<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
let token = tokens[ws] let token = tokens[ws]
if (token === undefined && getMetadata(presentation.metadata.Token) !== undefined) { if (token === undefined && getMetadata(presentation.metadata.Token) !== undefined) {
const selectWorkspace = await getResource(login.function.SelectWorkspace) const selectWorkspace = await getResource(login.function.SelectWorkspace)
const loginInfo = await ctx.with('select-workspace', {}, async () => (await selectWorkspace(ws))[1]) const loginInfo = await ctx.with('select-workspace', {}, async () => (await selectWorkspace(ws))[1])
@ -73,6 +75,22 @@ export async function connect (title: string): Promise<Client | undefined> {
} }
} }
setMetadata(presentation.metadata.Token, token) 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<void>((resolve) => setTimeout(resolve, 1000))
}
}
document.cookie = document.cookie =
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/' encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/'

View File

@ -43,7 +43,8 @@ export default mergeIds(workbenchId, workbench, {
NewVersionAvailable: '' as IntlString, NewVersionAvailable: '' as IntlString,
PleaseUpdate: '' as IntlString, PleaseUpdate: '' as IntlString,
MobileNotSupported: '' as IntlString, MobileNotSupported: '' as IntlString,
LogInAnyway: '' as IntlString LogInAnyway: '' as IntlString,
WorkspaceCreating: '' as IntlString
}, },
metadata: { metadata: {
MobileAllowed: '' as Metadata<boolean> MobileAllowed: '' as Metadata<boolean>

View File

@ -33,6 +33,8 @@ import view from '@hcengineering/view'
import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench' import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
export const workspaceCreating = writable<number | undefined>(undefined)
export function getSpecialSpaceClass (model: NavigatorModel): Array<Ref<Class<Space>>> { export function getSpecialSpaceClass (model: NavigatorModel): Array<Ref<Class<Space>>> {
const spaceResult = model.spaces.map((x) => x.spaceClass) const spaceResult = model.spaces.map((x) => x.spaceClass)
const result = (model.specials ?? []) const result = (model.specials ?? [])

View File

@ -14,7 +14,7 @@
// limitations under the License. // 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 accountEn from '@hcengineering/account/lang/en.json'
import accountRu from '@hcengineering/account/lang/ru.json' import accountRu from '@hcengineering/account/lang/ru.json'
import { registerProviders } from '@hcengineering/auth-providers' import { registerProviders } from '@hcengineering/auth-providers'
@ -93,6 +93,9 @@ export function serveAccount (measureCtx: MeasureContext, methods: Record<string
void client.then((p: MongoClient) => { void client.then((p: MongoClient) => {
const db = p.db(ACCOUNT_DB) const db = p.db(ACCOUNT_DB)
registerProviders(measureCtx, app, router, db, productId, serverSecret, frontURL) 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 => { const extractToken = (header: IncomingHttpHeaders): string | undefined => {

View File

@ -31,6 +31,7 @@ import core, {
generateId, generateId,
getWorkspaceId, getWorkspaceId,
MeasureContext, MeasureContext,
MeasureMetricsContext,
RateLimiter, RateLimiter,
Ref, Ref,
systemAccountEmail, systemAccountEmail,
@ -112,6 +113,9 @@ export interface Workspace {
lastVisit: number lastVisit: number
createdBy: string createdBy: string
creating?: boolean
createProgress?: number // Some progress
} }
/** /**
@ -129,6 +133,9 @@ export interface LoginInfo {
export interface WorkspaceLoginInfo extends LoginInfo { export interface WorkspaceLoginInfo extends LoginInfo {
workspace: string workspace: string
productId: string productId: string
creating?: boolean
createProgress?: number
} }
/** /**
@ -344,12 +351,14 @@ export async function selectWorkspace (
email, email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)), token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
workspace: workspaceUrl, workspace: workspaceUrl,
productId productId,
creating: workspaceInfo.creating,
createProgress: workspaceInfo.createProgress
} }
} }
if (workspaceInfo !== null) { if (workspaceInfo !== null) {
if (workspaceInfo.disabled === true) { if (workspaceInfo.disabled === true && workspaceInfo.creating !== true) {
await ctx.error('workspace disabled', { workspaceUrl, email }) await ctx.error('workspace disabled', { workspaceUrl, email })
throw new PlatformError( throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }) new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })
@ -364,7 +373,9 @@ export async function selectWorkspace (
email, email,
token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)), token: generateToken(email, getWorkspaceId(workspaceInfo.workspace, productId), getExtra(accountInfo)),
workspace: workspaceUrl, workspace: workspaceUrl,
productId productId,
creating: workspaceInfo.creating,
createProgress: workspaceInfo.createProgress
} }
return result return result
} }
@ -675,6 +686,19 @@ export async function setWorkspaceDisabled (db: Db, workspaceId: Workspace['_id'
await db.collection<Workspace>(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $set: { disabled } }) await db.collection<Workspace>(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $set: { disabled } })
} }
export async function cleanInProgressWorkspaces (db: Db, productId: string): Promise<void> {
const toDelete = (
await db
.collection<Workspace>(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 * @public
*/ */
@ -737,6 +761,8 @@ async function generateWorkspaceRecord (
workspaceName, workspaceName,
accounts: [], accounts: [],
disabled: true, disabled: true,
creating: true,
createProgress: 0,
createdOn: Date.now(), createdOn: Date.now(),
lastVisit: Date.now(), lastVisit: Date.now(),
createdBy: email createdBy: email
@ -767,6 +793,8 @@ async function generateWorkspaceRecord (
workspaceName, workspaceName,
accounts: [], accounts: [],
disabled: true, disabled: true,
creating: true,
createProgress: 0,
createdOn: Date.now(), createdOn: Date.now(),
lastVisit: Date.now(), lastVisit: Date.now(),
createdBy: email createdBy: email
@ -808,7 +836,8 @@ export async function createWorkspace (
productId: string, productId: string,
email: string, email: string,
workspaceName: string, workspaceName: string,
workspace?: string workspace?: string,
notifyHandler?: (workspace: Workspace) => void
): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client }> { ): Promise<{ workspaceInfo: Workspace, err?: any, client?: Client }> {
return await rateLimiter.exec(async () => { return await rateLimiter.exec(async () => {
// We need to search for duplicate workspaceUrl // We need to search for duplicate workspaceUrl
@ -818,6 +847,18 @@ export async function createWorkspace (
searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace) searchPromise = generateWorkspaceRecord(db, email, productId, version, workspaceName, workspace)
const workspaceInfo = await searchPromise const workspaceInfo = await searchPromise
notifyHandler?.(workspaceInfo)
const wsColl = db.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
async function updateInfo (ops: Partial<Workspace>): Promise<void> {
await wsColl.updateOne({ _id: workspaceInfo._id }, { $set: ops })
console.log('update', ops)
}
await updateInfo({ createProgress: 10 })
let client: Client | undefined let client: Client | undefined
const childLogger = ctx.newChild( const childLogger = ctx.newChild(
'createWorkspace', 'createWorkspace',
@ -838,24 +879,54 @@ export async function createWorkspace (
const wsId = getWorkspaceId(workspaceInfo.workspace, productId) const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) { if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) {
// Just any valid model for transactor to be able to function // 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. // Clone init workspace.
await cloneWorkspace( await cloneWorkspace(
getTransactor(), getTransactor(),
getWorkspaceId(initWS, productId), 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 { } 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) { } catch (err: any) {
return { workspaceInfo, err, client: null as any } return { workspaceInfo, err, client: null as any }
} }
// Workspace is created, we need to clear disabled flag. // Workspace is created, we need to clear disabled flag.
await db await updateInfo({ createProgress: 100, disabled: false, creating: false })
.collection<Omit<Workspace, '_id'>>(WORKSPACE_COLLECTION)
.updateOne({ _id: workspaceInfo._id }, { $set: { disabled: false } })
return { workspaceInfo, client } return { workspaceInfo, client }
}) })
} }
@ -901,7 +972,16 @@ export async function upgradeWorkspace (
} }
) )
await ( 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() ).close()
return versionStr return versionStr
} }
@ -933,42 +1013,56 @@ export const createUserWorkspace =
} }
} }
const { workspaceInfo, err, client } = await createWorkspace( async function doCreate (info: Account, notifyHandler: (workspace: Workspace) => void): Promise<void> {
ctx, const { workspaceInfo, err, client } = await createWorkspace(
version, ctx,
txes, version,
migrationOperation, txes,
db, migrationOperation,
productId, db,
email, productId,
workspaceName email,
) workspaceName,
undefined,
if (err != null) { notifyHandler
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. if (err != null) {
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } }) await ctx.error('failed to create workspace', { err, workspaceName, email })
// We need to drop workspace, to prevent wrong data usage.
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) await db.collection(WORKSPACE_COLLECTION).updateOne(
const shouldUpdateAccount = initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null {
await assignWorkspace(ctx, db, productId, email, workspaceInfo.workspace, shouldUpdateAccount, client) _id: workspaceInfo._id
await setRole(email, workspaceInfo.workspace, productId, AccountRole.Owner, client) },
} finally { { $set: { disabled: true, message: JSON.stringify(err?.message ?? ''), err: JSON.stringify(err) } }
await client?.close() )
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<Workspace>((resolve) => {
void doCreate(info, (info: Workspace) => {
resolve(info)
})
})
await assignWorkspaceRaw(db, { account: info, workspace: workspaceInfo })
const result = { const result = {
endpoint: getEndpoint(), endpoint: getEndpoint(),
email, email,
@ -976,7 +1070,7 @@ export const createUserWorkspace =
productId, productId,
workspace: workspaceInfo.workspaceUrl workspace: workspaceInfo.workspaceUrl
} }
await ctx.info('Creating workspace done', { workspaceName, email }) await ctx.info('Creating user side done', { workspaceName, email })
return result return result
} }
@ -1051,7 +1145,7 @@ export async function getUserWorkspaces (
.find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } })) .find(withProductId(productId, account.admin === true ? {} : { _id: { $in: account.workspaces } }))
.toArray() .toArray()
) )
.filter((it) => it.disabled !== true) .filter((it) => it.disabled !== true || it.creating === true)
.map(mapToClientWorkspace) .map(mapToClientWorkspace)
} }
@ -1094,7 +1188,7 @@ export async function getWorkspaceInfo (
const [ws] = ( const [ws] = (
await db.collection<Workspace>(WORKSPACE_COLLECTION).find(withProductId(productId, query)).toArray() await db.collection<Workspace>(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) { if (ws == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
} }
@ -1198,6 +1292,13 @@ export async function assignWorkspace (
} }
// Add account into workspace. // 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<void> {
await db await db
.collection(WORKSPACE_COLLECTION) .collection(WORKSPACE_COLLECTION)
.updateOne({ _id: workspaceInfo.workspace._id }, { $addToSet: { accounts: workspaceInfo.account._id } }) .updateOne({ _id: workspaceInfo.workspace._id }, { $addToSet: { accounts: workspaceInfo.account._id } })
@ -1206,9 +1307,6 @@ export async function assignWorkspace (
await db await db
.collection(ACCOUNT_COLLECTION) .collection(ACCOUNT_COLLECTION)
.updateOne({ _id: workspaceInfo.account._id }, { $addToSet: { workspaces: workspaceInfo.workspace._id } }) .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<Ref<Person>> { async function createEmployee (ops: TxOperations, name: string, _email: string): Promise<Ref<Person>> {

View File

@ -206,7 +206,8 @@ export async function cloneWorkspace (
transactorUrl: string, transactorUrl: string,
sourceWorkspaceId: WorkspaceId, sourceWorkspaceId: WorkspaceId,
targetWorkspaceId: WorkspaceId, targetWorkspaceId: WorkspaceId,
clearTime: boolean = true clearTime: boolean = true,
progress: (value: number) => Promise<void>
): Promise<void> { ): Promise<void> {
const sourceConnection = (await connect(transactorUrl, sourceWorkspaceId, undefined, { const sourceConnection = (await connect(transactorUrl, sourceWorkspaceId, undefined, {
mode: 'backup' mode: 'backup'
@ -220,6 +221,7 @@ export async function cloneWorkspace (
.domains() .domains()
.filter((it) => it !== DOMAIN_TRANSIENT && it !== DOMAIN_MODEL) .filter((it) => it !== DOMAIN_TRANSIENT && it !== DOMAIN_MODEL)
let i = 0
for (const c of domains) { for (const c of domains) {
console.log('clone domain...', c) console.log('clone domain...', c)
@ -322,6 +324,9 @@ export async function cloneWorkspace (
continue continue
} }
} }
i++
await progress((100 / domains.length) * i)
} }
} catch (err: any) { } catch (err: any) {
console.error(err) console.error(err)

View File

@ -112,8 +112,8 @@ export async function initModel (
rawTxes: Tx[], rawTxes: Tx[],
migrateOperations: [string, MigrateOperation][], migrateOperations: [string, MigrateOperation][],
logger: ModelLogger = consoleModelLogger, logger: ModelLogger = consoleModelLogger,
skipOperations: boolean = false progress: (value: number) => Promise<void>
): Promise<CoreClient | undefined> { ): Promise<CoreClient> {
const { mongodbUri, storageAdapter: minio, txes } = prepareTools(rawTxes) const { mongodbUri, storageAdapter: minio, txes } = prepareTools(rawTxes)
if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { if (txes.some((tx) => tx.objectSpace !== core.space.Model)) {
throw Error('Model txes must target only 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[]) const result = await db.collection(DOMAIN_TX).insertMany(txes as Document[])
logger.log('model transactions inserted.', { count: result.insertedCount }) logger.log('model transactions inserted.', { count: result.insertedCount })
await progress(10)
logger.log('creating data...', { transactorUrl }) logger.log('creating data...', { transactorUrl })
const { model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId) const { model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId)
await progress(20)
logger.log('create minio bucket', { workspaceId }) logger.log('create minio bucket', { workspaceId })
if (!(await minio.exists(ctx, workspaceId))) { if (!(await minio.exists(ctx, workspaceId))) {
await minio.make(ctx, workspaceId) await minio.make(ctx, workspaceId)
} }
if (!skipOperations) { connection = (await connect(
connection = (await connect( transactorUrl,
transactorUrl, workspaceId,
workspaceId, undefined,
undefined, {
{ model: 'upgrade',
model: 'upgrade', admin: 'true'
admin: 'true' },
}, model
model )) as unknown as CoreClient & BackupClient
)) as unknown as CoreClient & BackupClient
try { try {
for (const op of migrateOperations) { let i = 0
logger.log('Migrate', { name: op[0] }) for (const op of migrateOperations) {
await op[1].upgrade(connection, logger) logger.log('Migrate', { name: op[0] })
} await op[1].upgrade(connection, logger)
i++
// Create update indexes await progress(20 + (((100 / migrateOperations.length) * i) / 100) * 10)
await createUpdateIndexes(ctx, connection, db, logger)
} catch (e: any) {
logger.error('error', { error: e })
throw e
} }
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 { } finally {
_client.close() _client.close()
} }
@ -177,7 +186,8 @@ export async function upgradeModel (
rawTxes: Tx[], rawTxes: Tx[],
migrateOperations: [string, MigrateOperation][], migrateOperations: [string, MigrateOperation][],
logger: ModelLogger = consoleModelLogger, logger: ModelLogger = consoleModelLogger,
skipTxUpdate: boolean = false skipTxUpdate: boolean = false,
progress: (value: number) => Promise<void>
): Promise<CoreClient> { ): Promise<CoreClient> {
const { mongodbUri, txes } = prepareTools(rawTxes) const { mongodbUri, txes } = prepareTools(rawTxes)
@ -193,6 +203,7 @@ export async function upgradeModel (
if (!skipTxUpdate) { if (!skipTxUpdate) {
logger.log('removing model...', { workspaceId: workspaceId.name }) logger.log('removing model...', { workspaceId: workspaceId.name })
await progress(10)
// we're preserving accounts (created by core.account.System). // we're preserving accounts (created by core.account.System).
const result = await ctx.with( const result = await ctx.with(
'mongo-delete', 'mongo-delete',
@ -214,16 +225,20 @@ export async function upgradeModel (
) )
logger.log('model transactions inserted.', { workspaceId: workspaceId.name, count: insert.insertedCount }) logger.log('model transactions inserted.', { workspaceId: workspaceId.name, count: insert.insertedCount })
await progress(20)
} }
const { hierarchy, modelDb, model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId) const { hierarchy, modelDb, model } = await fetchModelFromMongo(ctx, mongodbUri, workspaceId)
await ctx.with('migrate', {}, async () => { await ctx.with('migrate', {}, async () => {
const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger) const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger)
let i = 0
for (const op of migrateOperations) { for (const op of migrateOperations) {
const t = Date.now() const t = Date.now()
await op[1].migrate(migrateClient, logger) await op[1].migrate(migrateClient, logger)
logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t }) 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 }) logger.log('Apply upgrade operations', { workspaceId: workspaceId.name })
@ -245,16 +260,23 @@ export async function upgradeModel (
) )
) )
// Create update indexes if (!skipTxUpdate) {
await ctx.with('create-indexes', {}, async (ctx) => { // Create update indexes
await createUpdateIndexes(ctx, connection, db, logger) 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 () => { await ctx.with('upgrade', {}, async () => {
let i = 0
for (const op of migrateOperations) { for (const op of migrateOperations) {
const t = Date.now() const t = Date.now()
await op[1].upgrade(connection, logger) await op[1].upgrade(connection, logger)
logger.log('upgrade:', { operation: op[0], time: Date.now() - t, workspaceId: workspaceId.name }) 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 return connection
@ -295,7 +317,8 @@ async function createUpdateIndexes (
ctx: MeasureContext, ctx: MeasureContext,
connection: CoreClient, connection: CoreClient,
db: Db, db: Db,
logger: ModelLogger logger: ModelLogger,
progress: (value: number) => Promise<void>
): Promise<void> { ): Promise<void> {
const classes = await ctx.with('find-classes', {}, async () => await connection.findAll(core.class.Class, {})) 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() async () => await db.listCollections({}, { nameOnly: true }).toArray()
) )
const promises: Promise<void>[] = []
let completed = 0
for (const [d, v] of domains.entries()) { for (const [d, v] of domains.entries()) {
const collInfo = collections.find((it) => it.name === d) const collInfo = collections.find((it) => it.name === d)
if (collInfo == null) { if (collInfo == null) {
@ -352,13 +377,25 @@ async function createUpdateIndexes (
const name = typeof vv === 'string' ? `${key}_1` : `${key}_${vv[key]}` const name = typeof vv === 'string' ? `${key}_1` : `${key}_${vv[key]}`
const exists = await collection.indexExists(name) const exists = await collection.indexExists(name)
if (!exists) { if (!exists) {
await collection.createIndex(vv)
bb.push(vv) bb.push(vv)
} }
} catch (err: any) { } catch (err: any) {
logger.error('error: failed to create index', { d, vv, err }) 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) { if (bb.length > 0) {
logger.log('created indexes', { d, bb }) logger.log('created indexes', { d, bb })
} }

View File

@ -17,6 +17,7 @@ import core, {
TxFactory, TxFactory,
WorkspaceEvent, WorkspaceEvent,
generateId, generateId,
systemAccountEmail,
toWorkspaceString, toWorkspaceString,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
@ -183,6 +184,7 @@ class TSessionManager implements SessionManager {
workspace: string workspace: string
workspaceUrl?: string | null workspaceUrl?: string | null
workspaceName?: string workspaceName?: string
creating?: boolean
}> { }> {
const userInfo = await ( const userInfo = await (
await fetch(accounts, { await fetch(accounts, {
@ -219,6 +221,10 @@ class TSessionManager implements SessionManager {
let workspaceInfo = await ctx.with('check-token', {}, async (ctx) => let workspaceInfo = await ctx.with('check-token', {}, async (ctx) =>
accountsUrl !== '' ? await this.getWorkspaceInfo(accountsUrl, rawToken) : this.wsFromToken(token) 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') { if (workspaceInfo === undefined && token.extra?.admin !== 'true') {
// No access to workspace for token. // No access to workspace for token.
return { error: new Error(`No access to workspace for token ${token.email} ${token.workspace.name}`) } 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 workspace: string
workspaceUrl?: string | null workspaceUrl?: string | null
workspaceName?: string workspaceName?: string
creating?: boolean
} { } {
return { return {
workspace: token.workspace.name, workspace: token.workspace.name,