mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 01:40:32 +00:00
QFIX: Admin panel show inactive workspaces (#8715)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
619fb62ed6
commit
0cdb235749
@ -8,11 +8,18 @@
|
||||
isRestoringMode,
|
||||
isUpgradingMode,
|
||||
reduceCalls,
|
||||
systemAccountEmail,
|
||||
versionToString,
|
||||
type BaseWorkspaceInfo
|
||||
} 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,
|
||||
@ -49,15 +56,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',
|
||||
@ -71,6 +80,30 @@
|
||||
|
||||
$: void updateWorkspaces($ticker)
|
||||
|
||||
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 !== systemAccountEmail).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(
|
||||
(it) =>
|
||||
@ -78,7 +111,8 @@
|
||||
(it.workspaceUrl?.includes(search) ?? false) ||
|
||||
it.workspace?.includes(search) ||
|
||||
it.createdBy?.includes(search)) &&
|
||||
(showSelectedRegionOnly ? it.region === selectedRegionId : true) &&
|
||||
(showSelectedRegionOnly ? it.region === filterRegionId : true) &&
|
||||
(showInactive ? isWorkspaceInactive(it, statsByWorkspace.get(it.workspace)) : true) &&
|
||||
((showActive && isActiveMode(it.mode)) ||
|
||||
(showArchived && isArchivingMode(it.mode)) ||
|
||||
(showDeleted && isDeletingMode(it.mode)) ||
|
||||
@ -90,11 +124,16 @@
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (sortingRule) {
|
||||
case SortingRule.Activity: {
|
||||
const aStats = statsByWorkspace.get(a.workspace ?? '')
|
||||
const bStats = statsByWorkspace.get(b.workspace ?? '')
|
||||
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)
|
||||
}
|
||||
@ -107,6 +146,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) => {
|
||||
@ -168,7 +225,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],
|
||||
@ -188,12 +247,13 @@
|
||||
let showArchived: boolean = false
|
||||
let showDeleted: boolean = false
|
||||
let showOther: boolean = true
|
||||
let showInactive: boolean = false
|
||||
|
||||
let showSelectedRegionOnly: boolean = false
|
||||
|
||||
$: groupped = groupByArray(sortedWorkspaces, (it) => {
|
||||
const lastUsageDays = Math.round((now - it.lastVisit) / (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)) / (1000 * 3600 * 24)) / 10
|
||||
return Object.entries(dayRanges).find(([_k, v]) => v[0] < lastUsageDays && lastUsageDays <= v[1])?.[0] ?? 'Years'
|
||||
})
|
||||
|
||||
let regionInfo: RegionInfo[] = []
|
||||
@ -201,6 +261,9 @@
|
||||
let regionTitles: Record<string, string> = {}
|
||||
|
||||
let selectedRegionId: string = ''
|
||||
|
||||
let filterRegionId: string = ''
|
||||
|
||||
void getRegionInfo().then((_regionInfo) => {
|
||||
regionInfo = _regionInfo ?? []
|
||||
regionTitles = Object.fromEntries(
|
||||
@ -209,6 +272,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)
|
||||
@ -219,6 +285,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) / (1000 * 3600 * 24))
|
||||
@ -246,10 +320,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]}
|
||||
@ -289,8 +367,8 @@
|
||||
<CheckBox bind:checked={showOther} />
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<span class="mr-2">Show selected region only:</span>
|
||||
<CheckBox bind:checked={showSelectedRegionOnly} />
|
||||
<span class="mr-2">Show inactive workspaces:</span>
|
||||
<CheckBox bind:checked={showInactive} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -322,13 +400,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))}
|
||||
@ -413,8 +510,9 @@
|
||||
</svelte:fragment>
|
||||
{#each v.slice(0, limit) as workspace}
|
||||
{@const wsName = workspace.workspaceName ?? workspace.workspace}
|
||||
{@const lastUsageDays = Math.round((now - workspace.lastVisit) / (1000 * 3600 * 24))}
|
||||
{@const lastUsageDays = Math.round((10 * (now - workspace.lastVisit)) / (1000 * 3600 * 24)) / 10}
|
||||
{@const bIdx = backupIdx.get(workspace.workspace)}
|
||||
{@const stats = statsByWorkspace.get(workspace.workspace ?? '')}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.workspace}`}>
|
||||
@ -435,6 +533,17 @@
|
||||
/>
|
||||
</div>
|
||||
{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}
|
||||
</span>
|
||||
<div class="ml-1" style:width={'18rem'}>
|
||||
{workspace.createdBy}
|
||||
@ -461,10 +570,7 @@
|
||||
</span>
|
||||
<span class="flex flex-between" style:width={'5rem'}>
|
||||
{#if workspace.backupInfo != null}
|
||||
{@const sz = Math.max(
|
||||
workspace.backupInfo.backupSize,
|
||||
workspace.backupInfo.dataSize + workspace.backupInfo.blobsSize
|
||||
)}
|
||||
{@const sz = getBackupSize(workspace)}
|
||||
{@const szGb = Math.round((sz * 100) / 1024) / 100}
|
||||
{#if szGb > 0}
|
||||
{Math.round((sz * 100) / 1024) / 100}Gb
|
||||
|
@ -73,12 +73,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 (hidden)' }).click()
|
||||
await page2.getByRole('button', { name: 'America', exact: true }).first().click()
|
||||
await page2.getByRole('button', { name: 'europe (hidden)' }).first().click()
|
||||
await page2.getByPlaceholder('Search').click()
|
||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
||||
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Archive' }).click()
|
||||
|
@ -72,12 +72,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 (hidden)' }).click()
|
||||
await page2.getByRole('button', { name: 'America', exact: true }).first().click()
|
||||
await page2.getByRole('button', { name: 'europe (hidden)' }).first().click()
|
||||
await page2.getByPlaceholder('Search').click()
|
||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
||||
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Migrate' }).click()
|
||||
|
Loading…
Reference in New Issue
Block a user