Skip to content

QFIX: Admin panel show inactive workspaces #8716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 128 additions & 15 deletions plugins/login-resources/src/components/AdminWorkspaces.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -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(
Expand All @@ -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)) ||
Expand All @@ -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)
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -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],
Expand All @@ -195,15 +255,18 @@
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[] = []

let regionTitles: Record<string, string> = {}

let selectedRegionId: string = ''

let filterRegionId: string = ''

void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo ?? []
regionTitles = Object.fromEntries(
Expand All @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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]}
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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))}
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 3 additions & 3 deletions ws-tests/sanity/tests/workspace/archive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions ws-tests/sanity/tests/workspace/migrate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading