* [ee] fix: serve index disk storage sizes from /srch/ endpoint On multi-container deployments, the API server doesn't have the index files on its local disk, so disk size was always reported as 0.0B. Added a new GET /srch/index/storage/disk endpoint that calculates disk sizes on the indexer process (which owns the files). The frontend now fetches disk sizes from this endpoint in parallel with the status call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 71aab648925f31cde37efd31d79a7f3a977fd42a This commit updates the EE repository reference after PR #480 was merged in windmill-ee-private. Previous ee-repo-ref: b3e0000e2528809302c18f36930aebf3d004747a New ee-repo-ref: 71aab648925f31cde37efd31d79a7f3a977fd42a Automated by sync-ee-ref workflow. * chore: update ee-repo-ref to indexer-disk-storage-zero branch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update sqlx metadata and ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
265 lines
8.2 KiB
Svelte
265 lines
8.2 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/common'
|
|
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
|
import { IndexSearchService } from '$lib/gen'
|
|
import type { GetIndexerStatusResponse, GetIndexDiskStorageSizesResponse } from '$lib/gen'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { displaySize } from '$lib/utils'
|
|
import Tooltip from '../Tooltip.svelte'
|
|
import IntegerInput from '../IntegerInput.svelte'
|
|
import InputError from '../InputError.svelte'
|
|
import Label from '../Label.svelte'
|
|
import { Loader2, RefreshCw } from 'lucide-svelte'
|
|
import type { Writable } from 'svelte/store'
|
|
|
|
interface Props {
|
|
values: Writable<Record<string, any>>
|
|
disabled?: boolean
|
|
errors?: Record<string, string>
|
|
}
|
|
|
|
let { values, disabled = false, errors = {} }: Props = $props()
|
|
|
|
let clearJobsIndexModalOpen = $state(false)
|
|
let clearServiceLogsIndexModalOpen = $state(false)
|
|
|
|
let status: GetIndexerStatusResponse | undefined = $state(undefined)
|
|
let diskSizes: GetIndexDiskStorageSizesResponse | undefined = $state(undefined)
|
|
let statusLoading = $state(true)
|
|
let statusError = $state(false)
|
|
|
|
function formatTimeAgo(isoDate: string): string {
|
|
const diffMs = Date.now() - new Date(isoDate).getTime()
|
|
const diffSecs = Math.floor(diffMs / 1000)
|
|
if (diffSecs < 60) return `${diffSecs}s ago`
|
|
const diffMins = Math.floor(diffSecs / 60)
|
|
if (diffMins < 60) return `${diffMins}m ago`
|
|
const diffHours = Math.floor(diffMins / 60)
|
|
return `${diffHours}h ago`
|
|
}
|
|
|
|
async function loadStatus() {
|
|
statusLoading = true
|
|
statusError = false
|
|
try {
|
|
const [statusRes, diskRes] = await Promise.all([
|
|
IndexSearchService.getIndexerStatus(),
|
|
IndexSearchService.getIndexDiskStorageSizes().catch(() => undefined)
|
|
])
|
|
status = statusRes
|
|
diskSizes = diskRes
|
|
} catch (e) {
|
|
status = undefined
|
|
diskSizes = undefined
|
|
statusError = true
|
|
} finally {
|
|
statusLoading = false
|
|
}
|
|
}
|
|
|
|
loadStatus()
|
|
</script>
|
|
|
|
<div class="space-y-6">
|
|
<div class="flex flex-col gap-1">
|
|
<label for="writer_memory_budget" class="block text-xs font-semibold text-emphasis">
|
|
Index writer memory budget (MB)
|
|
<Tooltip>
|
|
The allocated memory arena for the indexer. A bigger value means less writing to disk and
|
|
potentially higher indexing throughput
|
|
</Tooltip>
|
|
</label>
|
|
<IntegerInput
|
|
placeholder="300"
|
|
id="writer_memory_budget"
|
|
{disabled}
|
|
error={errors.writer_memory_budget ?? ''}
|
|
value={$values['indexer_settings'].writer_memory_budget != null
|
|
? $values['indexer_settings'].writer_memory_budget / (1024 * 1024)
|
|
: undefined}
|
|
oninput={(v) => {
|
|
if (v == null) {
|
|
const { writer_memory_budget: _, ...rest } = $values['indexer_settings']
|
|
$values['indexer_settings'] = rest
|
|
} else {
|
|
$values['indexer_settings'] = {
|
|
...$values['indexer_settings'],
|
|
writer_memory_budget: v * (1024 * 1024)
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
<InputError error={errors.writer_memory_budget ?? ''} />
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<label for="max_index_time_window_secs" class="block text-xs font-semibold text-emphasis">
|
|
Index time window (days)
|
|
<Tooltip>
|
|
Maximum age of items to include in the index. Jobs and logs older than this window will not
|
|
be indexed and will be cleaned up from the index. Set to 0 to disable (index everything
|
|
within the retention period).
|
|
</Tooltip>
|
|
</label>
|
|
<IntegerInput
|
|
placeholder="7"
|
|
id="max_index_time_window_secs"
|
|
{disabled}
|
|
error={errors.max_index_time_window_secs ?? ''}
|
|
value={$values['indexer_settings'].max_index_time_window_secs != null
|
|
? Math.round($values['indexer_settings'].max_index_time_window_secs / 86400)
|
|
: undefined}
|
|
oninput={(v) => {
|
|
if (v == null) {
|
|
const { max_index_time_window_secs: _, ...rest } = $values['indexer_settings']
|
|
$values['indexer_settings'] = rest
|
|
} else {
|
|
$values['indexer_settings'] = {
|
|
...$values['indexer_settings'],
|
|
max_index_time_window_secs: v * 86400
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
<InputError error={errors.max_index_time_window_secs ?? ''} />
|
|
</div>
|
|
<Label label="Indexer status">
|
|
{#snippet action()}
|
|
<button
|
|
onclick={loadStatus}
|
|
disabled={statusLoading}
|
|
class="inline-flex items-center text-secondary hover:text-primary disabled:opacity-50"
|
|
>
|
|
{#if statusLoading}
|
|
<Loader2 size={14} class="animate-spin" />
|
|
{:else}
|
|
<RefreshCw size={14} />
|
|
{/if}
|
|
</button>
|
|
{/snippet}
|
|
{#if status}
|
|
<div class="flex flex-col gap-2">
|
|
{#each [{ label: 'Job indexer', entry: status.job_indexer }, { label: 'Service log indexer', entry: status.log_indexer }] as { label, entry } (label)}
|
|
<div class="flex flex-row items-center gap-2 text-xs">
|
|
<span
|
|
class="inline-block w-2 h-2 rounded-full {entry?.is_alive
|
|
? 'bg-green-500'
|
|
: 'bg-red-500'}"
|
|
></span>
|
|
<span class="text-primary font-medium">{label}:</span>
|
|
<span
|
|
class="font-semibold {entry?.is_alive
|
|
? 'text-green-600 dark:text-green-400'
|
|
: 'text-red-600 dark:text-red-400'}"
|
|
>
|
|
{entry?.is_alive ? 'Running' : 'Stopped'}
|
|
</span>
|
|
{#if entry?.last_locked_at}
|
|
<span class="text-tertiary text-2xs">
|
|
Last active: {formatTimeAgo(entry.last_locked_at)}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else if statusError && !statusLoading}
|
|
<span class="text-2xs text-secondary">
|
|
Could not fetch indexer status. Search may not be enabled in this build.
|
|
</span>
|
|
{/if}
|
|
</Label>
|
|
<Label label="Index storage">
|
|
{#if status}
|
|
<div class="flex flex-col gap-1 text-2xs text-tertiary">
|
|
<span>
|
|
Jobs index:
|
|
{#if diskSizes?.job_index_disk_size_bytes != null}
|
|
Disk: {displaySize(diskSizes.job_index_disk_size_bytes) ?? 'N/A'}
|
|
{/if}
|
|
{#if status.job_indexer?.storage?.s3_size_bytes != null}
|
|
{#if diskSizes?.job_index_disk_size_bytes != null}·{/if}
|
|
S3: {displaySize(status.job_indexer.storage.s3_size_bytes) ?? 'N/A'}
|
|
{/if}
|
|
</span>
|
|
<span>
|
|
Service logs index:
|
|
{#if diskSizes?.log_index_disk_size_bytes != null}
|
|
Disk: {displaySize(diskSizes.log_index_disk_size_bytes) ?? 'N/A'}
|
|
{/if}
|
|
{#if status.log_indexer?.storage?.s3_size_bytes != null}
|
|
{#if diskSizes?.log_index_disk_size_bytes != null}·{/if}
|
|
S3: {displaySize(status.log_indexer.storage.s3_size_bytes) ?? 'N/A'}
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
</Label>
|
|
<Label label="Clear index">
|
|
<span class="text-xs text-secondary"
|
|
>These buttons will clear the whole index, and the service will start reindexing from scratch.
|
|
Full text search might be down during this time.</span
|
|
>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="flex flex-row items-center gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
onclick={() => {
|
|
clearJobsIndexModalOpen = true
|
|
}}
|
|
>
|
|
Clear jobs index
|
|
</Button>
|
|
</div>
|
|
<div class="flex flex-row items-center gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
onclick={() => {
|
|
clearServiceLogsIndexModalOpen = true
|
|
}}
|
|
>
|
|
Clear service logs index
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Label>
|
|
<ConfirmationModal
|
|
title="Clear jobs index"
|
|
confirmationText="Clear"
|
|
open={clearJobsIndexModalOpen}
|
|
type="danger"
|
|
on:canceled={() => {
|
|
clearJobsIndexModalOpen = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
const r = await IndexSearchService.clearIndex({
|
|
idxName: 'JobIndex'
|
|
})
|
|
sendUserToast(r)
|
|
clearJobsIndexModalOpen = false
|
|
}}
|
|
>
|
|
Are you sure you want to clear the jobs index? The service will start reindexing from scratch.
|
|
Full text search might be down during this time.
|
|
</ConfirmationModal>
|
|
<ConfirmationModal
|
|
title="Clear service logs index"
|
|
confirmationText="Clear"
|
|
open={clearServiceLogsIndexModalOpen}
|
|
type="danger"
|
|
on:canceled={() => {
|
|
clearServiceLogsIndexModalOpen = false
|
|
}}
|
|
on:confirmed={async () => {
|
|
const r = await IndexSearchService.clearIndex({
|
|
idxName: 'ServiceLogIndex'
|
|
})
|
|
sendUserToast(r)
|
|
clearServiceLogsIndexModalOpen = false
|
|
}}
|
|
>
|
|
Are you sure you want to clear the service logs index? The service will start reindexing from
|
|
scratch. Full text search might be down during this time.
|
|
</ConfirmationModal>
|
|
</div>
|