mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
UBERF-6469: Rework workspace creation to more informative (#5291)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
d971c46b28
commit
9419278973
@ -35,8 +35,9 @@
|
||||
</script>
|
||||
|
||||
<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} />
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -124,6 +124,9 @@
|
||||
<div class="flex flex-col flex-grow">
|
||||
<span class="label overflow-label flex-center">
|
||||
{wsName}
|
||||
{#if workspace.creating === true}
|
||||
({workspace.createProgress}%)
|
||||
{/if}
|
||||
</span>
|
||||
{#if isAdmin && wsName !== workspace.workspace}
|
||||
<span class="text-xs flex-center">
|
||||
|
@ -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
|
||||
|
@ -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<string, string> = fetchMetadataLocalStorage(login.metadata.LoginTokens) ?? {}
|
||||
tokens[loginInfo.workspace] = loginInfo.token
|
||||
|
@ -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<void>>,
|
||||
ChangePassword: '' as Resource<(oldPassword: string, password: string) => Promise<void>>,
|
||||
SelectWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
|
||||
FetchWorkspace: '' as Resource<(workspace: string) => Promise<[Status, WorkspaceLoginInfo | undefined]>>,
|
||||
GetWorkspaces: '' as Resource<() => Promise<Workspace[]>>,
|
||||
GetEndpoint: '' as Resource<() => Promise<string>>
|
||||
}
|
||||
|
@ -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..."
|
||||
}
|
||||
}
|
||||
|
@ -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..."
|
||||
}
|
||||
}
|
@ -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..."
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@
|
||||
"PleaseUpdate": "Пожалуйста, обновите приложение",
|
||||
"ServerUnderMaintenance": "Обслуживание сервера",
|
||||
"MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.",
|
||||
"LogInAnyway": "Все равно войти"
|
||||
"LogInAnyway": "Все равно войти",
|
||||
"WorkspaceCreating": "Пространство создается..."
|
||||
}
|
||||
}
|
||||
|
@ -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')}
|
||||
<Loading />
|
||||
<Loading>
|
||||
{#if ($workspaceCreating ?? -1) > 0}
|
||||
<div class="ml-1">
|
||||
<Label label={workbench.string.WorkspaceCreating} />
|
||||
{$workspaceCreating} %
|
||||
</div>
|
||||
{/if}
|
||||
</Loading>
|
||||
{:then client}
|
||||
{#if !client && versionError}
|
||||
<div class="version-wrapper">
|
||||
|
@ -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<Client | undefined> {
|
||||
}
|
||||
const tokens: Record<string, string> = 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<Client | undefined> {
|
||||
}
|
||||
}
|
||||
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 =
|
||||
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/'
|
||||
|
||||
|
@ -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<boolean>
|
||||
|
@ -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<number | undefined>(undefined)
|
||||
|
||||
export function getSpecialSpaceClass (model: NavigatorModel): Array<Ref<Class<Space>>> {
|
||||
const spaceResult = model.spaces.map((x) => x.spaceClass)
|
||||
const result = (model.specials ?? [])
|
||||
|
@ -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<string
|
||||
void client.then((p: MongoClient) => {
|
||||
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 => {
|
||||
|
@ -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>(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
|
||||
*/
|
||||
@ -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<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
|
||||
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<Omit<Workspace, '_id'>>(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
|
||||
)
|
||||
|
||||
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) } }
|
||||
async function doCreate (info: Account, notifyHandler: (workspace: Workspace) => void): Promise<void> {
|
||||
const { workspaceInfo, err, client } = await createWorkspace(
|
||||
ctx,
|
||||
version,
|
||||
txes,
|
||||
migrationOperation,
|
||||
db,
|
||||
productId,
|
||||
email,
|
||||
workspaceName,
|
||||
undefined,
|
||||
notifyHandler
|
||||
)
|
||||
throw err
|
||||
}
|
||||
try {
|
||||
info.lastWorkspace = Date.now()
|
||||
|
||||
// Update last workspace time.
|
||||
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: info._id }, { $set: { lastWorkspace: Date.now() } })
|
||||
if (err != null) {
|
||||
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)
|
||||
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<Workspace>((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>(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<void> {
|
||||
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<Ref<Person>> {
|
||||
|
@ -206,7 +206,8 @@ export async function cloneWorkspace (
|
||||
transactorUrl: string,
|
||||
sourceWorkspaceId: WorkspaceId,
|
||||
targetWorkspaceId: WorkspaceId,
|
||||
clearTime: boolean = true
|
||||
clearTime: boolean = true,
|
||||
progress: (value: number) => Promise<void>
|
||||
): Promise<void> {
|
||||
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)
|
||||
|
@ -112,8 +112,8 @@ export async function initModel (
|
||||
rawTxes: Tx[],
|
||||
migrateOperations: [string, MigrateOperation][],
|
||||
logger: ModelLogger = consoleModelLogger,
|
||||
skipOperations: boolean = false
|
||||
): Promise<CoreClient | undefined> {
|
||||
progress: (value: number) => Promise<void>
|
||||
): Promise<CoreClient> {
|
||||
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<void>
|
||||
): Promise<CoreClient> {
|
||||
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<void>
|
||||
): Promise<void> {
|
||||
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<void>[] = []
|
||||
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 })
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user