mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 18:01:59 +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,
|
isRestoringMode,
|
||||||
isUpgradingMode,
|
isUpgradingMode,
|
||||||
reduceCalls,
|
reduceCalls,
|
||||||
|
systemAccountEmail,
|
||||||
versionToString,
|
versionToString,
|
||||||
type BaseWorkspaceInfo
|
type BaseWorkspaceInfo
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
||||||
import { copyTextToClipboard, isAdminUser, MessageBox } from '@hcengineering/presentation'
|
import presentation, {
|
||||||
|
copyTextToClipboard,
|
||||||
|
isAdminUser,
|
||||||
|
MessageBox,
|
||||||
|
type OverviewStatistics,
|
||||||
|
type WorkspaceStatistics
|
||||||
|
} from '@hcengineering/presentation'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonMenu,
|
ButtonMenu,
|
||||||
@ -49,15 +56,17 @@
|
|||||||
let workspaces: WorkspaceInfo[] = []
|
let workspaces: WorkspaceInfo[] = []
|
||||||
|
|
||||||
enum SortingRule {
|
enum SortingRule {
|
||||||
Name = '1',
|
Activity = '1',
|
||||||
BackupDate = '2',
|
Name = '2',
|
||||||
BackupSize = '3',
|
BackupDate = '3',
|
||||||
LastVisit = '4'
|
BackupSize = '4',
|
||||||
|
LastVisit = '5'
|
||||||
}
|
}
|
||||||
|
|
||||||
let sortingRule = SortingRule.BackupDate
|
let sortingRule = SortingRule.Activity
|
||||||
|
|
||||||
const sortRules = {
|
const sortRules = {
|
||||||
|
[SortingRule.Activity]: 'Active users',
|
||||||
[SortingRule.Name]: 'Name',
|
[SortingRule.Name]: 'Name',
|
||||||
[SortingRule.BackupDate]: 'Backup date',
|
[SortingRule.BackupDate]: 'Backup date',
|
||||||
[SortingRule.BackupSize]: 'Backup size',
|
[SortingRule.BackupSize]: 'Backup size',
|
||||||
@ -71,6 +80,30 @@
|
|||||||
|
|
||||||
$: void updateWorkspaces($ticker)
|
$: 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
|
$: sortedWorkspaces = workspaces
|
||||||
.filter(
|
.filter(
|
||||||
(it) =>
|
(it) =>
|
||||||
@ -78,7 +111,8 @@
|
|||||||
(it.workspaceUrl?.includes(search) ?? false) ||
|
(it.workspaceUrl?.includes(search) ?? false) ||
|
||||||
it.workspace?.includes(search) ||
|
it.workspace?.includes(search) ||
|
||||||
it.createdBy?.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)) ||
|
((showActive && isActiveMode(it.mode)) ||
|
||||||
(showArchived && isArchivingMode(it.mode)) ||
|
(showArchived && isArchivingMode(it.mode)) ||
|
||||||
(showDeleted && isDeletingMode(it.mode)) ||
|
(showDeleted && isDeletingMode(it.mode)) ||
|
||||||
@ -90,11 +124,16 @@
|
|||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
switch (sortingRule) {
|
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: {
|
case SortingRule.BackupDate: {
|
||||||
return (a.backupInfo?.lastBackup ?? 0) - (b.backupInfo?.lastBackup ?? 0)
|
return (a.backupInfo?.lastBackup ?? 0) - (b.backupInfo?.lastBackup ?? 0)
|
||||||
}
|
}
|
||||||
case SortingRule.BackupSize:
|
case SortingRule.BackupSize:
|
||||||
return (b.backupInfo?.backupSize ?? 0) - (a.backupInfo?.backupSize ?? 0)
|
return getBackupSize(b) - getBackupSize(a)
|
||||||
case SortingRule.LastVisit:
|
case SortingRule.LastVisit:
|
||||||
return (b.lastVisit ?? 0) - (a.lastVisit ?? 0)
|
return (b.lastVisit ?? 0) - (a.lastVisit ?? 0)
|
||||||
}
|
}
|
||||||
@ -107,6 +146,24 @@
|
|||||||
|
|
||||||
let backupable: WorkspaceInfo[] = []
|
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
|
// Assign backup idx
|
||||||
const backupSorting = [...workspaces].filter((it) => {
|
const backupSorting = [...workspaces].filter((it) => {
|
||||||
@ -168,7 +225,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dayRanges = {
|
const dayRanges = {
|
||||||
Today: [-1, 1],
|
Hour: [-1, 0.1],
|
||||||
|
HalfDay: [0.1, 0.5],
|
||||||
|
Day: [0.5, 1],
|
||||||
Week: [1, 7],
|
Week: [1, 7],
|
||||||
Weeks: [7, 14],
|
Weeks: [7, 14],
|
||||||
Month: [14, 30],
|
Month: [14, 30],
|
||||||
@ -188,12 +247,13 @@
|
|||||||
let showArchived: boolean = false
|
let showArchived: boolean = false
|
||||||
let showDeleted: boolean = false
|
let showDeleted: boolean = false
|
||||||
let showOther: boolean = true
|
let showOther: boolean = true
|
||||||
|
let showInactive: boolean = false
|
||||||
|
|
||||||
let showSelectedRegionOnly: boolean = false
|
let showSelectedRegionOnly: boolean = false
|
||||||
|
|
||||||
$: groupped = groupByArray(sortedWorkspaces, (it) => {
|
$: groupped = groupByArray(sortedWorkspaces, (it) => {
|
||||||
const lastUsageDays = Math.round((now - it.lastVisit) / (1000 * 3600 * 24))
|
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] ?? 'Other'
|
return Object.entries(dayRanges).find(([_k, v]) => v[0] < lastUsageDays && lastUsageDays <= v[1])?.[0] ?? 'Years'
|
||||||
})
|
})
|
||||||
|
|
||||||
let regionInfo: RegionInfo[] = []
|
let regionInfo: RegionInfo[] = []
|
||||||
@ -201,6 +261,9 @@
|
|||||||
let regionTitles: Record<string, string> = {}
|
let regionTitles: Record<string, string> = {}
|
||||||
|
|
||||||
let selectedRegionId: string = ''
|
let selectedRegionId: string = ''
|
||||||
|
|
||||||
|
let filterRegionId: string = ''
|
||||||
|
|
||||||
void getRegionInfo().then((_regionInfo) => {
|
void getRegionInfo().then((_regionInfo) => {
|
||||||
regionInfo = _regionInfo ?? []
|
regionInfo = _regionInfo ?? []
|
||||||
regionTitles = Object.fromEntries(
|
regionTitles = Object.fromEntries(
|
||||||
@ -209,6 +272,9 @@
|
|||||||
if (selectedRegionId === '' && regionInfo.length > 0) {
|
if (selectedRegionId === '' && regionInfo.length > 0) {
|
||||||
selectedRegionId = regionInfo[0].region
|
selectedRegionId = regionInfo[0].region
|
||||||
}
|
}
|
||||||
|
if (filterRegionId === '' && regionInfo.length > 0) {
|
||||||
|
filterRegionId = regionInfo[0].region
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
|
$: selectedRegionRef = regionInfo.find((it) => it.region === selectedRegionId)
|
||||||
@ -219,6 +285,14 @@
|
|||||||
: selectedRegionRef.region
|
: selectedRegionRef.region
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
$: filteredRegionRef = regionInfo.find((it) => it.region === filterRegionId)
|
||||||
|
$: filteredRegionName =
|
||||||
|
filteredRegionRef !== undefined
|
||||||
|
? filteredRegionRef.name.length > 0
|
||||||
|
? filteredRegionRef.name
|
||||||
|
: filteredRegionRef.region
|
||||||
|
: ''
|
||||||
|
|
||||||
$: byVersion = groupByArray(
|
$: byVersion = groupByArray(
|
||||||
workspaces.filter((it) => {
|
workspaces.filter((it) => {
|
||||||
const lastUsed = Math.round((now - it.lastVisit) / (1000 * 3600 * 24))
|
const lastUsed = Math.round((now - it.lastVisit) / (1000 * 3600 * 24))
|
||||||
@ -246,10 +320,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="fs-title p-3">
|
<div class="fs-title p-3">
|
||||||
Workspaces: {workspaces.length} active: {workspaces.filter((it) => isActiveMode(it.mode)).length}
|
Workspaces: {workspaces.length} active: {workspaces.filter((it) => isActiveMode(it.mode)).length}
|
||||||
|
|
||||||
upgrading: {workspaces.filter((it) => isUpgradingMode(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)}
|
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">
|
<div class="flex-row-center">
|
||||||
{#each byVersion.entries() as [k, v]}
|
{#each byVersion.entries() as [k, v]}
|
||||||
@ -289,8 +367,8 @@
|
|||||||
<CheckBox bind:checked={showOther} />
|
<CheckBox bind:checked={showOther} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<span class="mr-2">Show selected region only:</span>
|
<span class="mr-2">Show inactive workspaces:</span>
|
||||||
<CheckBox bind:checked={showSelectedRegionOnly} />
|
<CheckBox bind:checked={showInactive} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -322,13 +400,32 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div class="fs-title p-1">
|
||||||
<Scroller maxHeight={40} noStretch={true}>
|
<Scroller maxHeight={40} noStretch={true}>
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
{#each Object.keys(dayRanges) as k}
|
{#each Object.keys(dayRanges) as k}
|
||||||
{@const v = groupped.get(k) ?? []}
|
{@const v = groupped.get(k) ?? []}
|
||||||
{@const hasMore = (groupped.get(k) ?? []).length > limit}
|
{@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 activeAll = v.filter((it) => isActiveMode(it.mode))}
|
||||||
{@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
|
{@const archivedV = v.filter((it) => isArchivingMode(it.mode))}
|
||||||
{@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
|
{@const deletedV = v.filter((it) => isDeletingMode(it.mode))}
|
||||||
@ -413,8 +510,9 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
{#each v.slice(0, limit) as workspace}
|
{#each v.slice(0, limit) as workspace}
|
||||||
{@const wsName = workspace.workspaceName ?? workspace.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 bIdx = backupIdx.get(workspace.workspace)}
|
||||||
|
{@const stats = statsByWorkspace.get(workspace.workspace ?? '')}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.workspace}`}>
|
<div class="flex fs-title cursor-pointer focused-button bordered" id={`${workspace.workspace}`}>
|
||||||
@ -435,6 +533,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{wsName}
|
{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>
|
</span>
|
||||||
<div class="ml-1" style:width={'18rem'}>
|
<div class="ml-1" style:width={'18rem'}>
|
||||||
{workspace.createdBy}
|
{workspace.createdBy}
|
||||||
@ -461,10 +570,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="flex flex-between" style:width={'5rem'}>
|
<span class="flex flex-between" style:width={'5rem'}>
|
||||||
{#if workspace.backupInfo != null}
|
{#if workspace.backupInfo != null}
|
||||||
{@const sz = Math.max(
|
{@const sz = getBackupSize(workspace)}
|
||||||
workspace.backupInfo.backupSize,
|
|
||||||
workspace.backupInfo.dataSize + workspace.backupInfo.blobsSize
|
|
||||||
)}
|
|
||||||
{@const szGb = Math.round((sz * 100) / 1024) / 100}
|
{@const szGb = Math.round((sz * 100) / 1024) / 100}
|
||||||
{#if szGb > 0}
|
{#if szGb > 0}
|
||||||
{Math.round((sz * 100) / 1024) / 100}Gb
|
{Math.round((sz * 100) / 1024) / 100}Gb
|
||||||
|
@ -73,12 +73,12 @@ test.describe('Workspace Archive tests', () => {
|
|||||||
const adminPage = new AdminPage(page2)
|
const adminPage = new AdminPage(page2)
|
||||||
await adminPage.gotoAdmin()
|
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(3) > .checkbox-container > .checkSVG').click()
|
||||||
await page2.locator('div:nth-child(4) > .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: 'America', exact: true }).first().click()
|
||||||
await page2.getByRole('button', { name: 'europe (hidden)' }).click()
|
await page2.getByRole('button', { name: 'europe (hidden)' }).first().click()
|
||||||
await page2.getByPlaceholder('Search').click()
|
await page2.getByPlaceholder('Search').click()
|
||||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
||||||
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Archive' }).click()
|
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)
|
const adminPage = new AdminPage(page2)
|
||||||
await adminPage.gotoAdmin()
|
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(3) > .checkbox-container > .checkSVG').click()
|
||||||
await page2.locator('div:nth-child(4) > .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: 'America', exact: true }).first().click()
|
||||||
await page2.getByRole('button', { name: 'europe (hidden)' }).click()
|
await page2.getByRole('button', { name: 'europe (hidden)' }).first().click()
|
||||||
await page2.getByPlaceholder('Search').click()
|
await page2.getByPlaceholder('Search').click()
|
||||||
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
await page2.getByPlaceholder('Search').fill(workspaceInfo.workspaceId)
|
||||||
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Migrate' }).click()
|
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Migrate' }).click()
|
||||||
|
Loading…
Reference in New Issue
Block a user