Skip to main content
Glama
VersionDetailsRow.tsx10 kB
import { type VersionSummary, isActiveStatus } from "../../store/types"; import VersionBadge from "./VersionBadge"; import LoadingSpinner from "./LoadingSpinner"; /** * Props for the VersionDetailsRow component. */ interface VersionDetailsRowProps { version: VersionSummary; libraryName: string; showDelete?: boolean; showRefresh?: boolean; } /** * Renders details for a single library version in a row format. * Includes version, stats, and optional delete/refresh buttons. * @param props - Component props including version, libraryName, showDelete, and showRefresh flags. */ const VersionDetailsRow = ({ version, libraryName, showDelete = true, showRefresh = false, }: VersionDetailsRowProps) => { // Format the indexed date nicely, handle null case const indexedDate = version.indexedAt ? new Date(version.indexedAt).toLocaleDateString() : "N/A"; // Display 'Latest' if version string is empty const versionLabel = version.ref.version || "Latest"; // Use empty string for latest version in param and rowId const versionParam = version.ref.version || ""; // Sanitize both libraryName and versionParam for valid CSS selector const sanitizedLibraryName = libraryName.replace(/[^a-zA-Z0-9-_]/g, "-"); const sanitizedVersionParam = versionParam.replace(/[^a-zA-Z0-9-_]/g, "-"); const rowId = `row-${sanitizedLibraryName}-${sanitizedVersionParam}`; // Determine initial isRefreshing based on version status const initialIsRefreshing = isActiveStatus(version.status); // Define state-specific button classes for Alpine toggling const defaultStateClasses = "text-red-700 border border-red-700 hover:bg-red-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-red-300 dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:focus:ring-red-800 dark:hover:bg-red-500"; const confirmingStateClasses = "bg-red-600 text-white border-red-600 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-700 dark:border-red-700 dark:focus:ring-red-800"; return ( // Use flexbox for layout, add border between rows <div id={rowId} class="flex justify-between items-center py-1 border-b border-gray-200 dark:border-gray-600 last:border-b-0" data-library-name={libraryName} data-version-param={versionParam} data-is-refreshing={initialIsRefreshing ? "true" : "false"} x-data="{ library: $el.dataset.libraryName, version: $el.dataset.versionParam, confirming: $el.dataset.confirming === 'true', isDeleting: false, isRefreshing: $el.dataset.isRefreshing === 'true', setRefreshing(val) { this.isRefreshing = !!val; this.$el.dataset.isRefreshing = val ? 'true' : 'false'; }, init() { const rowId = this.$el.id; const myLibrary = this.library; const myVersion = this.version; document.body.addEventListener('job-status-change', (e) => { const job = e.detail; const jobVersion = job.version || ''; if (job.library === myLibrary && jobVersion === myVersion) { const newValue = ['queued', 'running'].includes(job.status); const el = document.getElementById(rowId); if (el) { el.dispatchEvent(new CustomEvent('set-refreshing', { detail: newValue, bubbles: true })); } } }); } }" x-on:set-refreshing="setRefreshing($event.detail)" > {/* Version Label */} <span class="text-sm text-gray-900 dark:text-white w-1/4 truncate" title={versionLabel} > {version.ref.version ? ( <VersionBadge version={version.ref.version} /> ) : ( <span class="text-gray-600 dark:text-gray-400">Latest</span> )} </span> {/* Stats Group */} <div class="flex space-x-2 text-sm text-gray-600 dark:text-gray-400 w-3/4 justify-end items-center"> <span title="Number of unique pages indexed"> Pages:{" "} <span class="font-semibold" safe> {version.counts.uniqueUrls.toLocaleString()} </span> </span> <span title="Number of indexed snippets"> Chunks:{" "} <span class="font-semibold" safe> {version.counts.documents.toLocaleString()} </span> </span> <span title="Date last indexed"> Last Update:{" "} <span class="font-semibold" safe> {indexedDate} </span> </span> </div> {/* Action buttons container */} <div class="flex items-center ml-2 space-x-1"> {/* Refresh Button - Icon shown inline based on state */} {showRefresh && ( <> {/* Icon button - shown when NOT refreshing */} <template x-if="!isRefreshing"> <button type="button" class="font-medium rounded-lg text-sm p-1 w-6 h-6 text-center inline-flex items-center justify-center transition-colors duration-150 ease-in-out text-gray-500 border border-gray-300 hover:bg-gray-100 hover:text-gray-700 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:border-gray-600 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:hover:bg-gray-600" title="Refresh this version (re-scrape changed pages)" x-on:click=" isRefreshing = true; $root.dataset.isRefreshing = 'true'; $el.dispatchEvent(new CustomEvent('trigger-refresh', { bubbles: true })); " hx-post={`/web/libraries/${encodeURIComponent(libraryName)}/versions/${encodeURIComponent(versionParam)}/refresh`} hx-swap="none" hx-trigger="trigger-refresh" > <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> </svg> <span class="sr-only">Refresh version</span> </button> </template> {/* Spinner button - shown when refreshing */} <template x-if="isRefreshing"> <button type="button" class="font-medium rounded-lg text-sm p-1 w-6 h-6 text-center inline-flex items-center justify-center transition-colors duration-150 ease-in-out text-gray-500 border border-gray-300 dark:border-gray-600 dark:text-gray-400" title="Refresh in progress..." disabled > <LoadingSpinner class="text-gray-500 dark:text-gray-400" /> <span class="sr-only">Refreshing...</span> </button> </template> </> )} {/** * Conditionally renders a delete button for the version row. * The button has three states: * 1. Default: Displays a trash icon. * 2. Confirming: Displays a confirmation text with an accessible label. * 3. Deleting: Displays a spinner icon indicating the deletion process. * The button uses AlpineJS for state management and htmx for server interaction. */} {showDelete && ( <button type="button" class="font-medium rounded-lg text-sm p-1 min-w-6 h-6 text-center inline-flex items-center justify-center transition-colors duration-150 ease-in-out" title="Remove this version" x-bind:class={`confirming ? '${confirmingStateClasses}' : '${defaultStateClasses}'`} x-bind:disabled="isDeleting" x-on:click=" if (confirming) { isDeleting = true; window.confirmationManager.clear($root.id); $el.dispatchEvent(new CustomEvent('confirmed-delete', { bubbles: true })); } else { confirming = true; isDeleting = false; window.confirmationManager.start($root.id); } " hx-delete={`/web/libraries/${encodeURIComponent(libraryName)}/versions/${encodeURIComponent(versionParam)}`} hx-target={`#${rowId}`} hx-swap="outerHTML" hx-trigger="confirmed-delete" > {/* Default State: Trash Icon */} <span x-show="!confirming && !isDeleting"> <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20" > <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h16M7 8v8m4-8v8M7 1h4a1 1 0 0 1 1 1v3H6V2a1 1 0 0 1-1-1ZM3 5h12v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5Z" /> </svg> <span class="sr-only">Remove version</span> </span> {/* Confirming State: Text */} <span x-show="confirming && !isDeleting" class="mx-1"> Confirm?<span class="sr-only">Confirm delete</span> </span> {/* Deleting State: Spinner Icon */} <span x-show="isDeleting"> <LoadingSpinner /> <span class="sr-only">Loading...</span> </span> </button> )} </div> </div> ); }; export default VersionDetailsRow;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/arabold/docs-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server