+
+
+
+ Total
+ {workspaces.length}
- {/each}
-
-
- {#each byRegion.entries() as [k, v]}
-
- {k ?? ''}: {v.length}
+
+ Active
+ {workspaces.filter((it) => isActiveMode(it.mode)).length}
- {/each}
-
-
-
-
-
-
-
-
Filters:
-
- Show active workspaces:
-
-
-
- Show archived workspaces:
-
-
-
- Show deleted workspaces:
-
-
-
- Show other workspaces:
-
-
-
- Show attempts >=0 workspaces:
-
-
-
- Show selected region only:
-
-
-
- Show inactive workspaces:
-
-
-
-
-
- Sorting order: {sortingRule}
- ({ id: it[0], label: getEmbeddedLabel(it[1]) }))}
- on:selected={(it) => {
- sortingRule = it.detail
- }}
- />
-
-
-
- Migration region selector:
- ({
- id: it.region === '' ? '#' : it.region,
- label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
- }))}
- on:selected={(it) => {
- selectedRegionId = it.detail === '#' ? '' : it.detail
- }}
- />
-
-
-
-
-
-
-
Filtere region selector:
-
({
- id: it.region === '' ? '#' : it.region,
- label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
- }))}
- on:selected={(it) => {
- filterRegionId = it.detail === '#' ? '' : it.detail
- }}
- />
-
-
-
-
- {#each Object.keys(dayRanges) as k}
- {@const v = groupped.get(k) ?? []}
- {@const hasMore = (groupped.get(k) ?? []).length > limit}
- {@const activeV = v
- .filter((it) => isActiveMode(it.mode) && it.region !== selectedRegionId)
- .slice(0, limit)}
- {@const activeAll = v.filter((it) => isActiveMode(it.mode))}
- {@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
- {@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
- {@const maintenance = v.length - activeAll.length - archivedV.length - deletedV.length}
- {@const grByRegion = groupByArray(v, (it) => regionTitles[it.region ?? ''])}
- {#if v.length > 0}
-
0}>
-
-
- {k} -
- {#if hasMore}
- {limit} of {v.length}
+ {#if workspaces.filter((it) => isUpgradingMode(it.mode)).length > 0}
+
+ Upgrading
+ {workspaces.filter((it) => isUpgradingMode(it.mode)).length}
+
+ {/if}
+ {#if data != null}
+
+ With active sessions
+ {data.workspaces.length}
+
+ {/if}
+
+ Users
+ {data?.usersTotal ?? 0}
+
+
+ Connections
+ {data?.connectionsTotal ?? 0}
+
+
+ Created 30d
+ {createdLast30dCount}
+
+ {#if longRunningStatCount > 0}
+
+ Long-running > 1h
+ {longRunningStatCount}
+
+ {/if}
+
+
+
+ Total Storage
+ {totalStorageFormatted}
+ {#if totalStorageMb === 0}
+ No backup data yet
+ {/if}
+
+
+
+ {#if byVersion.size > 0 || byRegion.size > 0}
+
+ {#if byVersion.size > 0}
+
+ By version:
+ {#each byVersion.entries() as [k, v]}
+ {k}{v.length}
+ {/each}
+
+ {/if}
+ {#if byRegion.size > 0}
+
+ By region:
+ {#each byRegion.entries() as [k, v]}
+ {k ?? '—'}{v.length}
+ {/each}
+
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+ Showing {visibleWorkspaces.length} of {sortedWorkspaces.length} workspace{sortedWorkspaces.length !== 1
+ ? 's'
+ : ''}
+
+
+
+
+
+
+ 0 &&
+ visibleWorkspaces.every((w) => selectedWorkspaceUuids.has(w.uuid))}
+ on:value={(e) => {
+ toggleAllWsSelection(e.detail)
+ }}
+ />
+
+ {#each [{ field: 'name', label: 'Name', filter: true }, { field: 'region', label: 'Region', filter: true }, { field: 'last_visit', label: 'Last visit', filter: true, num: true }, { field: 'mode', label: 'Mode', filter: true }, { field: 'attempts', label: 'Attempts', filter: true, num: true }, { field: 'progress', label: 'Progress', filter: false, num: true }, { field: 'backup_size', label: 'Storage', filter: true }, { field: 'backup_age', label: 'Backup age', filter: true }] as col}
+
+ {
+ setSort(col.field)
+ }}
+ >
+ {#if sortField === col.field}{sortDir === 'asc' ? '↑' : '↓'}{/if}{col.label}
+
+ {#if col.filter}
+
+ {/if}
+
+ {/each}
+
Actions
+
+
+ {#if visibleWorkspaces.length === 0}
+
No workspaces match the current filters.
+ {:else}
+ {#each visibleWorkspaces as workspace, wsIdx (workspace.uuid)}
+ {@const wsName = workspace.name}
+ {@const lastUsageDays = Math.round((now - (workspace.lastVisit ?? 0)) / (1000 * 3600 * 24))}
+ {@const bIdx = backupIdx.get(workspace.uuid)}
+ {@const stats = statsByWorkspace.get(workspace.uuid ?? '')}
+
+
+
{
+ openWorkspace(workspace.uuid)
+ }}
+ >
+
+
+ {
+ toggleWsSelection(workspace.uuid, e.detail)
+ }}
+ />
+
+
+
{wsName}
+ {#if stats}
+
+ {stats.sessions?.length ?? 0}
+ ·
+ {sessionOpsByWs.get(workspace.uuid ?? '') ?? 0}
+
+ {/if}
+
+
+
+
{workspace.region ?? ''}
+
{lastUsageDays}d
+
+ {workspace.mode ?? '-'}
+
+
+ {workspace.processingAttempts}
+ {#if workspace.processingAttempts > 0}
+ {
+ showPopup(MessageBox, {
+ label: getEmbeddedLabel(`Reset attempts ${workspace.url}`),
+ message: getEmbeddedLabel('Please confirm'),
+ action: async () => {
+ await performWorkspaceOperation(workspace.uuid, 'reset-attempts')
+ }
+ })
+ }}
+ />
+ {/if}
+
+
+ {#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
+ {workspace.processingProgress}%
+ {/if}
+
+
+ {#if workspace.backupInfo != null}
+ {@const sz = Math.max(
+ workspace.backupInfo.backupSize,
+ workspace.backupInfo.dataSize + workspace.backupInfo.blobsSize
+ )}
+ {@const szGb = Math.round((sz * 100) / 1024) / 100}
+ {#if szGb > 0}
+ {szGb} GB
{:else}
- {v.length}
+ {Math.round(sz * 100) / 100} MB
{/if}
- {#if maintenance > 0}
- - maitenance: {maintenance}
+ {#if bIdx != null}
+
[#{bIdx}]
{/if}
- {#if grByRegion.size > 1}
- {#each grByRegion.entries() as [k, v]}
-
- {k ?? ''}: {v.length}
-
- {/each}
+ {:else}
+
—
+ {/if}
+
+
+ {#if workspace.backupInfo != null}
+ {@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))}
+ {#if hours > 24}
+ {Math.round(hours / 24)}d ago
+ {:else}
+ {hours}h ago
{/if}
-
-
-
- {#if hasMore}
-
- {
- limit += 50
- }}
- />
-
+ {:else}
+ —
{/if}
-
-
- {#if activeAll.length > 0}
+
+
+ {#if workspace.mode === 'active'}
{
showPopup(MessageBox, {
- label: getEmbeddedLabel(`Mass Archive ${activeAll.length}`),
- message: getEmbeddedLabel(`Please confirm archive ${activeAll.length} workspaces`),
+ label: getEmbeddedLabel(`Archive ${workspace.url}`),
+ message: getEmbeddedLabel('Please confirm'),
action: async () => {
- void performWorkspaceOperation(
- activeAll.map((it) => it.uuid),
- 'archive'
- )
+ await performWorkspaceOperation(workspace.uuid, 'archive')
}
})
}}
/>
{/if}
-
- {#if regionInfo.length > 0 && activeV.length > 0}
+ {#if workspace.mode === 'archived'}
+ {
+ showPopup(MessageBox, {
+ label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
+ message: getEmbeddedLabel('Please confirm'),
+ action: async () => {
+ await performWorkspaceOperation(workspace.uuid, 'unarchive')
+ }
+ })
+ }}
+ />
+ {/if}
+ {#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== migrateTargetRegionId}
{
showPopup(MessageBox, {
- label: getEmbeddedLabel(`Mass Migrate ${activeV.length}`),
- message: getEmbeddedLabel(`Please confirm migrate ${activeV.length} workspaces`),
+ label: getEmbeddedLabel(`Migrate ${workspace.url}`),
+ message: getEmbeddedLabel('Please confirm'),
action: async () => {
- await performWorkspaceOperation(
- activeV.map((it) => it.uuid),
- 'migrate-to',
- selectedRegionId
- )
+ await performWorkspaceOperation(workspace.uuid, 'migrate-to', migrateTargetRegionId)
}
})
}}
/>
{/if}
-
- {#each v.slice(0, limit) as workspace}
- {@const wsName = workspace.name}
- {@const lastUsageDays = Math.round((now - (workspace.lastVisit ?? 0)) / (1000 * 3600 * 24))}
- {@const bIdx = backupIdx.get(workspace.uuid)}
- {@const stats = statsByWorkspace.get(workspace.uuid ?? '')}
-
-
-
-
- {wsName}
- {#if stats}
- -
-
- {stats.sessions?.length ?? 0}
-
- {(stats.sessions ?? []).reduceRight(
- (p, it) => p + (it.mins5.tx + it.mins5.find) + (it.current.tx + it.current.find),
- 0
- )}
-
- {/if}
-
- select(workspace.url)}
- showTooltip={{ label: getEmbeddedLabel('Open Workspace URL') }}
- />
- copyTextToClipboard(workspace.uuid)}
- showTooltip={{ label: getEmbeddedLabel('Copy UUID') }}
- />
-
-
-
- {workspace.region ?? ''}
-
-
- {lastUsageDays} days
-
-
- {workspace.mode ?? '-'}
-
-
- {workspace.processingAttempts}
- {#if workspace.processingAttempts > 0}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Reset attempts ${workspace.url}`),
- message: getEmbeddedLabel('Please confirm'),
- action: async () => {
- await performWorkspaceOperation(workspace.uuid, 'reset-attempts')
- }
- })
- }}
- icon={IconDownOutline}
- size={'small'}
- kind={'ghost'}
- />
- {/if}
-
-
- {#if workspace.processingProgress !== 100 && workspace.processingProgress !== 0}
- ({workspace.processingProgress}%)
- {/if}
-
-
- {#if workspace.backupInfo != null}
- {@const sz = Math.max(
- workspace.backupInfo.backupSize,
- workspace.backupInfo.dataSize + workspace.backupInfo.blobsSize
- )}
- {@const szGb = Math.round((sz * 100) / 1024) / 100}
- {#if szGb > 0}
- {Math.round((sz * 100) / 1024) / 100}Gb
- {:else}
- {Math.round(sz * 100) / 100}Mb
- {/if}
- {/if}
- {#if bIdx != null}
- [#{bIdx}]
- {/if}
-
-
- {#if workspace.backupInfo != null}
- {@const hours = Math.round((now - workspace.backupInfo.lastBackup) / (1000 * 3600))}
-
- {#if hours > 24}
- {Math.round(hours / 24)} days
- {:else}
- {hours} hours
- {/if}
- {/if}
-
-
- {#if workspace.mode === 'active'}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Archive ${workspace.url}`),
- message: getEmbeddedLabel('Please confirm'),
- action: async () => {
- await performWorkspaceOperation(workspace.uuid, 'archive')
- }
- })
- }}
- />
- {/if}
-
- {#if workspace.mode === 'archived'}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Unarchive ${workspace.url}`),
- message: getEmbeddedLabel('Please confirm'),
- action: async () => {
- await performWorkspaceOperation(workspace.uuid, 'unarchive')
- }
- })
- }}
- />
- {/if}
- {#if regionInfo.length > 0 && workspace.mode === 'active' && (workspace.region ?? '') !== selectedRegionId}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Migrate ${workspace.url}`),
- message: getEmbeddedLabel('Please confirm'),
- action: async () => {
- await performWorkspaceOperation(workspace.uuid, 'migrate-to', selectedRegionId)
- }
- })
- }}
- />
- {/if}
-
- {#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Delete ${workspace.url}`),
- message: getEmbeddedLabel('Please confirm'),
- action: async () => {
- await performWorkspaceOperation(workspace.uuid, 'delete')
- }
- })
- }}
- />
- {/if}
-
-
- {/each}
-
- {/if}
- {/each}
-
-
-
-
-
-
-
Accounts administration panel
-
- Enable deletion
-
-
-
-
-
-
-
-
- {
- accountSkip = Math.max(0, accountSkip - accountLimit)
- await loadAccounts(accountSearch, accountSkip, accountLimit)
- }}
- />
- Page {Math.floor(accountSkip / accountLimit) + 1}
- {
- accountSkip += accountLimit
- await loadAccounts(accountSearch, accountSkip, accountLimit)
- }}
- />
-
-
-
-
-
- {#each accounts as account}
-
-
-
- {account.firstName}
- {account.lastName}
+ {#if superAdminMode && !isDeletingMode(workspace.mode) && !isArchivingMode(workspace.mode)}
+ {
+ showPopup(MessageBox, {
+ label: getEmbeddedLabel(`Delete ${workspace.url}`),
+ message: getEmbeddedLabel('Please confirm'),
+ action: async () => {
+ await performWorkspaceOperation(workspace.uuid, 'delete')
+ }
+ })
+ }}
+ />
+ {/if}
+
- {account.uuid}
-
+ {/each}
+ {/if}
+
-
-
Social IDs: {account.socialIds.length}
- {#each account.socialIds as socialId}
-
{socialId.type}:{socialId.value}
- {/each}
-
-
-
Workspaces: {account.workspaces.length}
- {#each account.workspaces as workspace}
-
{workspace.name} {workspace.url} {workspace.uuid} {workspace.dataId}
- {/each}
-
-
- {#if accountSuperAdminMode}
- {
- showPopup(MessageBox, {
- label: getEmbeddedLabel(`Delete account ${account.firstName} ${account.lastName}`),
- message: getEmbeddedLabel('Please confirm account deletion. This action cannot be undone.'),
- action: async () => {
- await deleteAccount(account.uuid)
- await loadAccounts(accountSearch, accountSkip, accountLimit)
- }
- })
- }}
- />
- {/if}
-
-
- {/each}
+ {#if hasMore}
+
+ {
+ limit += 50
+ }}
+ />
+
+ {/if}
-
-
+
+ {#if selectedWorkspaceUuid != null}
+
+ {/if}
{/if}
+
+
diff --git a/plugins/login-resources/src/components/ForceLogoutModal.svelte b/plugins/login-resources/src/components/ForceLogoutModal.svelte
new file mode 100644
index 00000000000..6b1e32b2298
--- /dev/null
+++ b/plugins/login-resources/src/components/ForceLogoutModal.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
Reason: {reason}
+
+
+
+
+
+
+
diff --git a/plugins/login-resources/src/components/LoginApp.svelte b/plugins/login-resources/src/components/LoginApp.svelte
index 2af6f572bd3..4554f8ca9f5 100644
--- a/plugins/login-resources/src/components/LoginApp.svelte
+++ b/plugins/login-resources/src/components/LoginApp.svelte
@@ -25,7 +25,8 @@
getCurrentLocation,
location,
setMetadataLocalStorage,
- themeStore
+ themeStore,
+ location as locationStore
} from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import { onDestroy, onMount } from 'svelte'
@@ -53,7 +54,13 @@
import loginBackWebp from '../../img/login_back.webp'
import loginBack2xWebp from '../../img/login_back_2x.webp'
import AdminWorkspaces from './AdminWorkspaces.svelte'
+ import AdminUsers from './AdminUsers.svelte'
+ import AdminAudit from './AdminAudit.svelte'
import ChangePassword from './ChangePassword.svelte'
+ import ForceLogoutModal from './ForceLogoutModal.svelte'
+ import { forceLogoutReason } from '../utils'
+
+ $: subPath = $locationStore.path[2]
export let page: Pages = 'signup'
@@ -122,8 +129,19 @@
onMount(chooseToken)
+{#if $forceLogoutReason != null}
+
+{/if}
+
{#if page === 'admin'}
-
+ {#if subPath === 'users'}
+
+ {:else if subPath === 'audit'}
+
+ {:else}
+
+ {/if}
+
{:else}