Skip to content
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
150 changes: 128 additions & 22 deletions plugins/login-resources/src/components/AdminWorkspaces.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -71,14 +80,39 @@

$: 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) =>
((it.workspaceName?.includes(search) ?? false) ||
(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)) ||
Expand All @@ -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)
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -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],
Expand All @@ -188,19 +247,23 @@
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[] = []

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

let selectedRegionId: string = ''

let filterRegionId: string = ''

void getRegionInfo().then((_regionInfo) => {
regionInfo = _regionInfo ?? []
regionTitles = Object.fromEntries(
Expand All @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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]}
Expand Down Expand Up @@ -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>

Expand Down Expand Up @@ -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))}
Expand Down Expand Up @@ -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}`}>
Expand All @@ -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}
Expand All @@ -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
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 @@ -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()
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 @@ -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()
Expand Down
Loading