QFIX: Admin panel show inactive workspaces (#8716)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-04-28 20:18:54 +07:00 committed by GitHub
parent c31815e19f
commit 0b73f61106
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 21 deletions

View File

@ -9,11 +9,18 @@
isRestoringMode,
isUpgradingMode,
reduceCalls,
systemAccountUuid,
versionToString,
type WorkspaceInfoWithStatus
} from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { copyTextToClipboard, isAdminUser, MessageBox } from '@hcengineering/presentation'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation, {
copyTextToClipboard,
isAdminUser,
MessageBox,
type OverviewStatistics,
type WorkspaceStatistics
} from '@hcengineering/presentation'
import {
Button,
ButtonMenu,
@ -51,15 +58,17 @@
let workspaces: WorkspaceInfo[] = []
enum SortingRule {
Name = '1',
BackupDate = '2',
BackupSize = '3',
LastVisit = '4'
Activity = '1',
Name = '2',
BackupDate = '3',
BackupSize = '4',
LastVisit = '5'
}
let sortingRule = SortingRule.BackupDate
let sortingRule = SortingRule.Activity
const sortRules = {
[SortingRule.Activity]: 'Active users',
[SortingRule.Name]: 'Name',
[SortingRule.BackupDate]: 'Backup date',
[SortingRule.BackupSize]: 'Backup size',
@ -81,6 +90,31 @@
let showOther: boolean = true
let showGrAttempts: boolean = true
let showSelectedRegionOnly: boolean = false
let showInactive = false
function isWorkspaceInactive (it: WorkspaceInfo, stats: WorkspaceStatistics | undefined): boolean {
if (stats === undefined) {
return true
}
const ops = (stats.sessions ?? []).reduceRight(
(p, it) => p + (it.mins5.tx + it.mins5.find) + (it.current.tx + it.current.find),
0
)
if (ops === 0) {
return true
}
if (stats.sessions.filter((it) => (it.userId as any) !== systemAccountUuid).length === 0) {
return true
}
return false
}
function getBackupSize (workspace: WorkspaceInfo): number {
return Math.max(
workspace.backupInfo?.backupSize ?? 0,
(workspace.backupInfo?.dataSize ?? 0) + (workspace.backupInfo?.blobsSize ?? 0)
)
}
$: sortedWorkspaces = workspaces
.filter(
@ -89,7 +123,8 @@
(it.url?.includes(search) ?? false) ||
it.uuid?.includes(search) ||
it.createdBy?.includes(search)) &&
(showSelectedRegionOnly ? it.region === selectedRegionId : true) &&
(showSelectedRegionOnly ? it.region === filterRegionId : true) &&
(showInactive ? isWorkspaceInactive(it, statsByWorkspace.get(it.uuid)) : true) &&
((showActive && isActiveMode(it.mode)) ||
(showArchived && isArchivingMode(it.mode)) ||
(showDeleted && isDeletingMode(it.mode)) ||
@ -102,11 +137,16 @@
)
.sort((a, b) => {
switch (sortingRule) {
case SortingRule.Activity: {
const aStats = statsByWorkspace.get(a.uuid ?? '')
const bStats = statsByWorkspace.get(b.uuid ?? '')
return (bStats?.sessions?.length ?? 0) - (aStats?.sessions?.length ?? 0)
}
case SortingRule.BackupDate: {
return (a.backupInfo?.lastBackup ?? 0) - (b.backupInfo?.lastBackup ?? 0)
}
case SortingRule.BackupSize:
return (b.backupInfo?.backupSize ?? 0) - (a.backupInfo?.backupSize ?? 0)
return getBackupSize(b) - getBackupSize(a)
case SortingRule.LastVisit:
return (b.lastVisit ?? 0) - (a.lastVisit ?? 0)
}
@ -119,6 +159,24 @@
let backupable: WorkspaceInfo[] = []
const token: string = getMetadata(presentation.metadata.Token) ?? ''
const endpoint = getMetadata(presentation.metadata.StatsUrl)
async function fetchStats (time: number): Promise<void> {
await fetch(endpoint + `/api/v1/overview?token=${token}`, {})
.then(async (json) => {
data = await json.json()
})
.catch((err) => {
console.error(err)
})
}
let data: OverviewStatistics | undefined
$: void fetchStats($ticker)
$: statsByWorkspace = new Map((data?.workspaces ?? []).map((it) => [it.wsId, it]))
$: {
// Assign backup idx
const backupSorting = [...workspaces].filter((it) => {
@ -180,7 +238,9 @@
}
const dayRanges = {
Today: [-1, 1],
Hour: [-1, 0.1],
HalfDay: [0.1, 0.5],
Day: [0.5, 1],
Week: [1, 7],
Weeks: [7, 14],
Month: [14, 30],
@ -195,8 +255,8 @@
let limit = 50
$: groupped = groupByArray(sortedWorkspaces, (it) => {
const lastUsageDays = Math.round((now - (it.lastVisit ?? 0)) / (1000 * 3600 * 24))
return Object.entries(dayRanges).find(([_k, v]) => v[0] < lastUsageDays && lastUsageDays < v[1])?.[0] ?? 'Other'
const lastUsageDays = Math.round((10 * (now - (it.lastVisit ?? 0))) / (1000 * 3600 * 24)) / 10
return Object.entries(dayRanges).find(([_k, v]) => v[0] < lastUsageDays && lastUsageDays <= v[1])?.[0] ?? 'Years'
})
let regionInfo: RegionInfo[] = []
@ -204,6 +264,9 @@
let regionTitles: Record<string, string> = {}
let selectedRegionId: string = ''
let filterRegionId: string = ''
void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo ?? []
regionTitles = Object.fromEntries(
@ -212,6 +275,9 @@
if (selectedRegionId === '' && regionInfo.length > 0) {
selectedRegionId = regionInfo[0].region
}
if (filterRegionId === '' && regionInfo.length > 0) {
filterRegionId = regionInfo[0].region
}
})
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
@ -222,6 +288,14 @@
: selectedRegionRef.region
: ''
$: filteredRegionRef = regionInfo.find((it) => it.region === filterRegionId)
$: filteredRegionName =
filteredRegionRef !== undefined
? filteredRegionRef.name.length > 0
? filteredRegionRef.name
: filteredRegionRef.region
: ''
$: byVersion = groupByArray(
workspaces.filter((it) => {
const lastUsed = Math.round((now - (it.lastVisit ?? 0)) / (1000 * 3600 * 24))
@ -249,10 +323,14 @@
</div>
<div class="fs-title p-3">
Workspaces: {workspaces.length} active: {workspaces.filter((it) => isActiveMode(it.mode)).length}
upgrading: {workspaces.filter((it) => isUpgradingMode(it.mode)).length}
<br />
Backupable: {backupable.length} new: {backupable.reduce((p, it) => p + (it.backupInfo == null ? 1 : 0), 0)}
Active: {data?.workspaces.length ?? -1}
<br />
<span class="mt-2">
Users: {data?.usersTotal}/{data?.connectionsTotal}
</span>
<div class="flex-row-center">
{#each byVersion.entries() as [k, v]}
@ -299,6 +377,10 @@
<span class="mr-2">Show selected region only:</span>
<CheckBox bind:checked={showSelectedRegionOnly} />
</div>
<div class="flex-row-center">
<span class="mr-2">Show inactive workspaces:</span>
<CheckBox bind:checked={showInactive} />
</div>
</div>
<div class="fs-title p-3 flex-row-center">
@ -329,13 +411,32 @@
}}
/>
</div>
<div class="fs-title p-3 flex-row-center">
<div class="mr-2">
<CheckBox bind:checked={showSelectedRegionOnly} />
</div>
<span class="mr-2"> Filtere region selector: </span>
<ButtonMenu
selected={filterRegionId}
autoSelectionIfOne
title={filteredRegionName}
items={regionInfo.map((it) => ({
id: it.region === '' ? '#' : it.region,
label: getEmbeddedLabel(it.name.length > 0 ? it.name : it.region + ' (hidden)')
}))}
on:selected={(it) => {
filterRegionId = it.detail === '#' ? '' : it.detail
}}
/>
</div>
<div class="fs-title p-1">
<Scroller maxHeight={40} noStretch={true}>
<div class="mr-4">
{#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)}
{@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))}
@ -422,11 +523,23 @@
{@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 ?? '')}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<tr class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.uuid}`}>
<div class="label overflow-label p-1 flex flex-row-center" style:width={'15rem'}>
{wsName}
{#if stats}
-
<div class="ml-1">
{stats.sessions?.length ?? 0}
{(stats.sessions ?? []).reduceRight(
(p, it) => p + (it.mins5.tx + it.mins5.find) + (it.current.tx + it.current.find),
0
)}
</div>
{/if}
<div class="ml-1 flex flex-row-center">
<Button
icon={IconOpen}

View File

@ -76,12 +76,12 @@ test.describe('Workspace Archive tests', () => {
const adminPage = new AdminPage(page2)
await adminPage.gotoAdmin()
await page2.getByText('Today -').click()
await page2.getByText('Hour -').click()
await page2.locator('div:nth-child(3) > .checkbox-container > .checkSVG').click()
await page2.locator('div:nth-child(4) > .checkbox-container > .checkSVG').click()
await page2.getByRole('button', { name: 'America', exact: true }).click()
await page2.getByRole('button', { name: 'europe' }).click()
await page2.getByRole('button', { name: 'America', exact: true }).first().click()
await page2.getByRole('button', { name: 'europe' }).first().click()
await page2.getByPlaceholder('Search').click()
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspace)
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByRole('button', { name: 'Archive' }).click()

View File

@ -77,12 +77,12 @@ test.describe('Workspace Migration tests', () => {
const adminPage = new AdminPage(page2)
await adminPage.gotoAdmin()
await page2.getByText('Today -').click()
await page2.getByText('Hour -').click()
await page2.locator('div:nth-child(3) > .checkbox-container > .checkSVG').click()
await page2.locator('div:nth-child(4) > .checkbox-container > .checkSVG').click()
await page2.getByRole('button', { name: 'America', exact: true }).click()
await page2.getByRole('button', { name: 'europe' }).click()
await page2.getByRole('button', { name: 'America', exact: true }).first().click()
await page2.getByRole('button', { name: 'europe' }).first().click()
await page2.getByPlaceholder('Search').click()
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspace)
await page2.locator(`[id="${workspaceInfo.workspace}"]`).getByRole('button', { name: 'Migrate' }).click()