diff --git a/.vscode/launch.json b/.vscode/launch.json index 30c6efb309..18b03ba8b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,7 +55,7 @@ "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", // "SERVER_PROVIDER":"uweb" "SERVER_PROVIDER":"ws", - "MODEL_VERSION": "0.6.382", + "MODEL_VERSION": "0.6.421", // "VERSION": "0.6.289", "ELASTIC_INDEX_NAME": "local_storage_index", "UPLOAD_URL": "/files", @@ -166,7 +166,7 @@ "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin", "MINIO_ENDPOINT": "localhost", - "MODEL_VERSION": "v0.6.382", + "MODEL_VERSION": "v0.6.421", "WS_OPERATION": "all+backup", "BACKUP_STORAGE": "minio|minio?accessKey=minioadmin&secretKey=minioadmin", "BACKUP_BUCKET": "dev-backups", @@ -189,6 +189,7 @@ // "DB_URL": "mongodb://localhost:27017", // "DB_URL": "postgresql://postgres:example@localhost:5432", "DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable", + "FULLTEXT_URL": "http://host.docker.internal:4702", "REGION": "cockroach", "SERVER_SECRET": "secret", "TRANSACTOR_URL": "ws://localhost:3332", @@ -198,7 +199,7 @@ "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin", "MINIO_ENDPOINT": "localhost", - "MODEL_VERSION": "v0.6.382", + "MODEL_VERSION": "0.6.421", "WS_OPERATION": "all+backup", "BACKUP_STORAGE": "minio|minio?accessKey=minioadmin&secretKey=minioadmin", "BACKUP_BUCKET": "dev-backups", @@ -326,7 +327,7 @@ "ACCOUNTS_URL": "http://localhost:3000", "TELEGRAM_DATABASE": "telegram-service", "REKONI_URL": "http://localhost:4004", - "MODEL_VERSION": "0.6.382" + "MODEL_VERSION": "0.6.421" }, "runtimeVersion": "20", "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], @@ -353,7 +354,7 @@ "MONGO_URL": "mongodb://localhost:27017", "TELEGRAM_DATABASE": "telegram-service", "REKONI_URL": "http://localhost:4004", - "MODEL_VERSION": "0.6.382" + "MODEL_VERSION": "0.6.421" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "sourceMaps": true, @@ -397,7 +398,7 @@ "SECRET": "secret", "REGION": "cockroach", "BUCKET_NAME":"backups", - "INTERVAL":"30" + "INTERVAL":"43200" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "showAsyncStacks": true, diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 83067cabf3..8405dbc581 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -495,6 +495,7 @@ services: - MONGO_URL=${MONGO_URL} - ACCOUNTS_URL=http://host.docker.internal:3000 - SUPPORT_WORKSPACE=support + - STORAGE_CONFIG=${STORAGE_CONFIG} - FIRST_NAME=Jolie - LAST_NAME=AI - PASSWORD=password diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 4cbfc2311a..e8d22f42eb 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -709,7 +709,7 @@ export function isActiveMode (mode?: WorkspaceMode): boolean { return mode === 'active' } export function isDeletingMode (mode: WorkspaceMode): boolean { - return mode === 'pending-deletion' || mode === 'deleting' + return mode === 'pending-deletion' || mode === 'deleting' || mode === 'deleted' } export function isArchivingMode (mode?: WorkspaceMode): boolean { return ( @@ -751,6 +751,8 @@ export type WorkspaceUpdateEvent = | 'archiving-clean-started' | 'archiving-clean-done' | 'archiving-done' + | 'delete-started' + | 'delete-done' export interface BackupStatus { dataSize: number diff --git a/plugins/login-resources/src/components/AdminWorkspaces.svelte b/plugins/login-resources/src/components/AdminWorkspaces.svelte index 19e7905bab..09d86739bd 100644 --- a/plugins/login-resources/src/components/AdminWorkspaces.svelte +++ b/plugins/login-resources/src/components/AdminWorkspaces.svelte @@ -10,10 +10,11 @@ type BaseWorkspaceInfo } from '@hcengineering/core' import { getEmbeddedLabel } from '@hcengineering/platform' - import { isAdminUser } from '@hcengineering/presentation' + import { isAdminUser, MessageBox } from '@hcengineering/presentation' import { Button, ButtonMenu, + CheckBox, Expandable, IconArrowRight, IconOpen, @@ -23,12 +24,14 @@ Popup, Scroller, SearchEdit, - ticker, - CheckBox + showPopup, + ticker } from '@hcengineering/ui' import { workbenchId } from '@hcengineering/workbench' import { getAllWorkspaces, getRegionInfo, performWorkspaceOperation, type RegionInfo } from '../utils' + $: now = $ticker + $: isAdmin = isAdminUser() let search: string = '' @@ -42,16 +45,119 @@ let workspaces: WorkspaceInfo[] = [] + enum SortingRule { + Name = '1', + BackupDate = '2', + BackupSize = '3', + LastVisit = '4' + } + + let sortingRule = SortingRule.BackupDate + + const sortRules = { + [SortingRule.Name]: 'Name', + [SortingRule.BackupDate]: 'Backup date', + [SortingRule.BackupSize]: 'Backup size', + [SortingRule.LastVisit]: 'Last visit' + } + const updateWorkspaces = reduceCalls(async (_: number) => { const res = await getAllWorkspaces() - workspaces = res.sort((a, b) => - (b.workspaceUrl ?? b.workspace).localeCompare(a.workspaceUrl ?? a.workspace) - ) as WorkspaceInfo[] + workspaces = res as WorkspaceInfo[] }) $: void updateWorkspaces($ticker) - const now = Date.now() + $: sortedWorkspaces = workspaces + .filter( + (it) => + ((it.workspaceName?.includes(search) ?? false) || + (it.workspaceUrl?.includes(search) ?? false) || + it.workspace?.includes(search) || + it.createdBy?.includes(search)) && + ((showActive && isActiveMode(it.mode)) || + (showArchived && isArchivingMode(it.mode)) || + (showDeleted && isDeletingMode(it.mode)) || + (showOther && (isMigrationMode(it.mode) || isRestoringMode(it.mode)))) + ) + .sort((a, b) => { + switch (sortingRule) { + case SortingRule.BackupDate: { + return (a.backupInfo?.lastBackup ?? 0) - (b.backupInfo?.lastBackup ?? 0) + } + case SortingRule.BackupSize: + return (b.backupInfo?.backupSize ?? 0) - (a.backupInfo?.backupSize ?? 0) + case SortingRule.LastVisit: + return (b.lastVisit ?? 0) - (a.lastVisit ?? 0) + } + return (b.workspaceUrl ?? b.workspace).localeCompare(a.workspaceUrl ?? a.workspace) + }) + + let backupIdx = new Map() + + const backupInterval: number = 43200 + + let backupable: WorkspaceInfo[] = [] + + $: { + // Assign backup idx + const backupSorting = [...sortedWorkspaces].filter((it) => { + if (!isActiveMode(it.mode)) { + return false + } + const lastBackup = it.backupInfo?.lastBackup ?? 0 + if ((now - lastBackup) / 1000 < backupInterval) { + // No backup required, interval not elapsed + return false + } + + const createdOn = Math.floor((now - it.createdOn) / 1000) + if (createdOn <= 2) { + // Skip if we created is less 2 days + return false + } + if (it.lastVisit == null) { + return false + } + + const lastVisitSec = Math.floor((now - it.lastVisit) / 1000) + if (lastVisitSec > backupInterval) { + // No backup required, interval not elapsed + return false + } + return true + }) + const newBackupIdx = new Map() + + backupSorting.sort((a, b) => { + return (a.backupInfo?.lastBackup ?? 0) - (b.backupInfo?.lastBackup ?? 0) + }) + + // Shift new with existing ones. + const existingNew = groupByArray(backupSorting, (it) => it.backupInfo != null) + + const existing = existingNew.get(true) ?? [] + const newOnes = existingNew.get(false) ?? [] + const mixedBackupSorting: WorkspaceInfo[] = [] + + while (existing.length > 0 || newOnes.length > 0) { + const e = existing.shift() + const n = newOnes.shift() + if (e != null) { + mixedBackupSorting.push(e) + } + if (n != null) { + mixedBackupSorting.push(n) + } + } + + backupable = mixedBackupSorting + + for (const [idx, it] of mixedBackupSorting.entries()) { + newBackupIdx.set(it.workspace, idx) + } + backupIdx = newBackupIdx + } const dayRanges = { Today: 1, @@ -72,32 +178,19 @@ let showActive: boolean = true let showArchived: boolean = false - let showDeleted: boolean = true + let showDeleted: boolean = false let showOther: boolean = true - $: groupped = groupByArray( - workspaces.filter( - (it) => - ((it.workspaceName?.includes(search) ?? false) || - (it.workspaceUrl?.includes(search) ?? false) || - it.workspace?.includes(search) || - it.createdBy?.includes(search)) && - ((showActive && isActiveMode(it.mode)) || - (showArchived && isArchivingMode(it.mode)) || - (showDeleted && isDeletingMode(it.mode)) || - (showOther && (isMigrationMode(it.mode) || isRestoringMode(it.mode)))) - ), - (it) => { - const lastUsageDays = Math.round((now - it.lastVisit) / (1000 * 3600 * 24)) - return Object.entries(dayRanges).find(([_k, v]) => lastUsageDays <= v)?.[0] ?? 'Other' - } - ) + $: groupped = groupByArray(sortedWorkspaces, (it) => { + const lastUsageDays = Math.round((now - it.lastVisit) / (1000 * 3600 * 24)) + return Object.entries(dayRanges).find(([_k, v]) => lastUsageDays <= v)?.[0] ?? 'Other' + }) let regionInfo: RegionInfo[] = [] let selectedRegionId: string = '' void getRegionInfo().then((_regionInfo) => { - regionInfo = _regionInfo ?? [] + regionInfo = _regionInfo?.filter((it) => it.name !== '') ?? [] if (selectedRegionId === '' && regionInfo.length > 0) { selectedRegionId = regionInfo[0].region } @@ -111,6 +204,8 @@
Workspaces administration panel
Workspaces: {workspaces.length} active: {workspaces.filter((it) => isActiveMode(it.mode)).length} + + Backupable: {backupable.length} new: {backupable.reduce((p, it) => p + (it.backupInfo == null ? 1 : 0), 0)}
@@ -136,6 +231,19 @@
+
+ Sorting order: {sortingRule} + ({ id: it[0], label: getEmbeddedLabel(it[1]) }))} + on:selected={(it) => { + sortingRule = it.detail + }} + /> +
+
Migration region selector: { - void performWorkspaceOperation( - archiveV.map((it) => it.workspace), - 'archive' - ) + showPopup(MessageBox, { + label: getEmbeddedLabel(`Mass Archive ${archiveV.length}`), + message: getEmbeddedLabel(`Please confirm archive ${archiveV.length} workspaces`), + action: async () => { + void performWorkspaceOperation( + archiveV.map((it) => it.workspace), + 'archive' + ) + } + }) }} /> {/if} @@ -207,18 +321,25 @@ kind={'positive'} label={getEmbeddedLabel(`Mass Migrate ${activeV.length} to ${selectedRegionName ?? ''}`)} on:click={() => { - void performWorkspaceOperation( - activeV.map((it) => it.workspace), - 'migrate-to', - selectedRegionId - ) + showPopup(MessageBox, { + label: getEmbeddedLabel(`Mass Migrate ${archiveV.length}`), + message: getEmbeddedLabel(`Please confirm migrate ${archiveV.length} workspaces`), + action: async () => { + await performWorkspaceOperation( + activeV.map((it) => it.workspace), + 'migrate-to', + selectedRegionId + ) + } + }) }} /> {/if} {#each v.slice(0, limit) as workspace} {@const wsName = workspace.workspaceName ?? workspace.workspace} - {@const lastUsageDays = Math.round((Date.now() - workspace.lastVisit) / (1000 * 3600 * 24))} + {@const lastUsageDays = Math.round((now - workspace.lastVisit) / (1000 * 3600 * 24))} + {@const bIdx = backupIdx.get(workspace.workspace)}
@@ -272,6 +393,9 @@ {Math.round(sz * 100) / 100}Mb {/if} {/if} + {#if bIdx != null} + [#{bIdx}] + {/if} {#if workspace.backupInfo != null} @@ -294,7 +418,13 @@ label={getEmbeddedLabel('Archive')} kind={'ghost'} on:click={() => { - void performWorkspaceOperation(workspace.workspace, 'archive') + showPopup(MessageBox, { + label: getEmbeddedLabel(`Archive ${workspace.workspaceUrl}`), + message: getEmbeddedLabel('Please confirm'), + action: async () => { + await performWorkspaceOperation(workspace.workspace, 'archive') + } + }) }} /> {/if} @@ -306,7 +436,13 @@ kind={'ghost'} label={getEmbeddedLabel('Unarchive')} on:click={() => { - void performWorkspaceOperation(workspace.workspace, 'unarchive') + showPopup(MessageBox, { + label: getEmbeddedLabel(`Unarchive ${workspace.workspaceUrl}`), + message: getEmbeddedLabel('Please confirm'), + action: async () => { + await performWorkspaceOperation(workspace.workspace, 'unarchive') + } + }) }} /> {/if} @@ -318,7 +454,31 @@ kind={'positive'} label={getEmbeddedLabel('Migrate ' + (selectedRegionName ?? ''))} on:click={() => { - void performWorkspaceOperation(workspace.workspace, 'migrate-to', selectedRegionId) + showPopup(MessageBox, { + label: getEmbeddedLabel(`Migrate ${workspace.workspaceUrl}`), + message: getEmbeddedLabel('Please confirm'), + action: async () => { + await performWorkspaceOperation(workspace.workspace, 'migrate-to', selectedRegionId) + } + }) + }} + /> + {/if} + + {#if !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)} +