@galaxy-tool-util/cache-ui 0.0.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,20 +1,46 @@
1
1
  {
2
2
  "name": "@galaxy-tool-util/cache-ui",
3
- "version": "0.0.0",
3
+ "version": "0.2.2",
4
4
  "description": "Vue 3 + PrimeVue panel for inspecting and managing the Galaxy tool cache exposed by gxwf-web / tool-cache-proxy",
5
5
  "type": "module",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/jmchilton/galaxy-tool-util-ts",
9
- "directory": "packages/cache-ui"
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "import": "./src/index.ts"
10
+ },
11
+ "./*.vue": "./src/*.vue",
12
+ "./cache-ui.css": "./src/cache-ui.css"
10
13
  },
11
- "license": "MIT",
12
14
  "files": [
15
+ "src",
13
16
  "README.md",
14
17
  "LICENSE"
15
18
  ],
19
+ "dependencies": {
20
+ "openapi-fetch": "^0.13.8",
21
+ "primevue": "^4.3.0",
22
+ "vue": "^3.5.0",
23
+ "@galaxy-tool-util/tool-cache-proxy": "1.13.1"
24
+ },
25
+ "devDependencies": {
26
+ "vue-tsc": "^2.2.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/jmchilton/galaxy-tool-util-ts",
31
+ "directory": "packages/cache-ui"
32
+ },
16
33
  "publishConfig": {
17
34
  "access": "public",
18
- "provenance": false
35
+ "provenance": true
36
+ },
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "typecheck": "vue-tsc --noEmit",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "lint": "eslint src/ test/",
43
+ "format": "prettier --check 'src/**/*.{ts,vue}' 'test/**/*.ts'",
44
+ "format-fix": "prettier --write 'src/**/*.{ts,vue}' 'test/**/*.ts'"
19
45
  }
20
- }
46
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Default values for the design tokens the cache-ui components reference.
3
+ * Optional — consumers that already define these (e.g. gxwf-ui via galaxy.css)
4
+ * can skip the import. Proxy SPA imports it to get a usable baseline.
5
+ */
6
+ :root {
7
+ --gx-mono: "Monaco", "Menlo", "Consolas", "Courier New", monospace;
8
+ --gx-sp-1: 0.25rem;
9
+ --gx-sp-2: 0.5rem;
10
+ --gx-sp-3: 0.75rem;
11
+ --gx-sp-4: 1rem;
12
+ --gx-sp-5: 1.25rem;
13
+ --gx-sp-6: 1.5rem;
14
+ --gx-sp-8: 2rem;
15
+ --gx-fs-xs: 0.75rem;
16
+ --gx-fs-sm: 0.85rem;
17
+ --gx-fs-base: 1rem;
18
+ --gx-fs-lg: 1.25rem;
19
+ --gx-fs-xl: 1.5rem;
20
+ }
package/src/client.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Typed cache client. Sources its compile-time types from
3
+ * `@galaxy-tool-util/tool-cache-proxy`'s OpenAPI spec — both `tool-cache-proxy`
4
+ * and `gxwf-web` implement the cache surface identically, so any server that
5
+ * speaks it will satisfy these types at runtime.
6
+ */
7
+
8
+ import createClient from "openapi-fetch";
9
+ import type { Client, ClientOptions } from "openapi-fetch";
10
+ import type { paths, components } from "@galaxy-tool-util/tool-cache-proxy";
11
+
12
+ export type { paths, components };
13
+ export type CachedToolEntry = components["schemas"]["CachedToolEntry"];
14
+ export type CacheStats = components["schemas"]["CacheStats"];
15
+ export type CacheClient = Client<paths>;
16
+
17
+ /**
18
+ * Create an HTTP client pointed at a server that exposes the shared cache
19
+ * surface (gxwf-web or tool-cache-proxy).
20
+ *
21
+ * @param baseUrl - Base URL of the server. Pass "" to use the current origin.
22
+ */
23
+ export function createCacheClient(
24
+ baseUrl: string,
25
+ options?: Omit<ClientOptions, "baseUrl">,
26
+ ): CacheClient {
27
+ return createClient<paths>({ baseUrl, ...options });
28
+ }
@@ -0,0 +1,196 @@
1
+ <template>
2
+ <Dialog
3
+ v-model:visible="visible"
4
+ :header="entry?.toolId ?? 'Cached payload'"
5
+ modal
6
+ :style="{ width: '70vw' }"
7
+ :breakpoints="{ '960px': '90vw' }"
8
+ >
9
+ <Tabs v-model:value="activeTab">
10
+ <TabList>
11
+ <Tab v-for="t in TABS" :key="t.id" :value="t.id">
12
+ {{ t.label }}
13
+ </Tab>
14
+ </TabList>
15
+ <TabPanels>
16
+ <TabPanel v-for="t in TABS" :key="t.id" :value="t.id">
17
+ <p v-if="t.id === 'tool_source'" class="source-note">XML macros are expanded.</p>
18
+ <div v-if="loading[t.id]" class="loading-state">Loading…</div>
19
+ <div v-else-if="errors[t.id]" class="error-state">{{ errors[t.id] }}</div>
20
+ <pre v-else class="raw-json">{{ pretty(t.id) }}</pre>
21
+ </TabPanel>
22
+ </TabPanels>
23
+ </Tabs>
24
+ <template #footer>
25
+ <Button label="Copy" icon="pi pi-copy" text :disabled="!activePretty" @click="copy" />
26
+ <Button label="Close" icon="pi pi-times" @click="visible = false" />
27
+ </template>
28
+ </Dialog>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed, reactive, ref, watch } from "vue";
33
+ import Button from "primevue/button";
34
+ import Dialog from "primevue/dialog";
35
+ import Tabs from "primevue/tabs";
36
+ import TabList from "primevue/tablist";
37
+ import Tab from "primevue/tab";
38
+ import TabPanels from "primevue/tabpanels";
39
+ import TabPanel from "primevue/tabpanel";
40
+ import { useToast } from "primevue/usetoast";
41
+ import type { CachedToolEntry as Entry } from "../client.js";
42
+ type TabId =
43
+ | "parameter_model"
44
+ | "parameter_request_schema"
45
+ | "parameter_landing_request_schema"
46
+ | "parameter_test_case_xml_schema"
47
+ | "tool_source";
48
+
49
+ interface Loaders {
50
+ tool_source: (toolId: string, toolVersion: string) => Promise<unknown>;
51
+ parameter_model: (toolId: string, toolVersion: string) => Promise<unknown>;
52
+ parameter_request_schema: (toolId: string, toolVersion: string) => Promise<unknown>;
53
+ parameter_landing_request_schema: (toolId: string, toolVersion: string) => Promise<unknown>;
54
+ parameter_test_case_xml_schema: (toolId: string, toolVersion: string) => Promise<unknown>;
55
+ }
56
+
57
+ const props = defineProps<{
58
+ modelValue: boolean;
59
+ entry: Entry | null;
60
+ loaders: Loaders;
61
+ }>();
62
+ const emit = defineEmits<{ "update:modelValue": [v: boolean] }>();
63
+
64
+ const visible = computed({
65
+ get: () => props.modelValue,
66
+ set: (v) => emit("update:modelValue", v),
67
+ });
68
+
69
+ const TABS: { id: TabId; label: string }[] = [
70
+ { id: "parameter_model", label: "Model" },
71
+ { id: "parameter_request_schema", label: "Request schema" },
72
+ { id: "parameter_landing_request_schema", label: "Landing-request schema" },
73
+ { id: "parameter_test_case_xml_schema", label: "Test-case schema" },
74
+ { id: "tool_source", label: "Source" },
75
+ ];
76
+
77
+ const toast = useToast();
78
+ const activeTab = ref<TabId>("parameter_model");
79
+ const contents = reactive<Record<TabId, unknown>>({
80
+ parameter_model: null,
81
+ parameter_request_schema: null,
82
+ parameter_landing_request_schema: null,
83
+ parameter_test_case_xml_schema: null,
84
+ tool_source: null,
85
+ });
86
+ const loading = reactive<Record<TabId, boolean>>({
87
+ parameter_model: false,
88
+ parameter_request_schema: false,
89
+ parameter_landing_request_schema: false,
90
+ parameter_test_case_xml_schema: false,
91
+ tool_source: false,
92
+ });
93
+ const errors = reactive<Record<TabId, string | null>>({
94
+ parameter_model: null,
95
+ parameter_request_schema: null,
96
+ parameter_landing_request_schema: null,
97
+ parameter_test_case_xml_schema: null,
98
+ tool_source: null,
99
+ });
100
+ const fetched = reactive<Record<TabId, boolean>>({
101
+ parameter_model: false,
102
+ parameter_request_schema: false,
103
+ parameter_landing_request_schema: false,
104
+ parameter_test_case_xml_schema: false,
105
+ tool_source: false,
106
+ });
107
+
108
+ function pretty(id: TabId): string {
109
+ const v = contents[id];
110
+ if (id === "tool_source") return typeof v === "string" ? v : "";
111
+ return v === null || v === undefined ? "" : JSON.stringify(v, null, 2);
112
+ }
113
+ const activePretty = computed(() => pretty(activeTab.value));
114
+
115
+ let entryGeneration = 0;
116
+
117
+ async function loadTab(id: TabId): Promise<void> {
118
+ if (fetched[id] || loading[id]) return;
119
+ if (!props.entry) return;
120
+ const generation = entryGeneration;
121
+ const entry = props.entry;
122
+ loading[id] = true;
123
+ errors[id] = null;
124
+ try {
125
+ const loader = props.loaders[id];
126
+ const result = await loader(entry.toolId, entry.requestVersion ?? entry.toolVersion);
127
+ if (generation !== entryGeneration) return;
128
+ contents[id] = result;
129
+ fetched[id] = true;
130
+ } catch (e) {
131
+ if (generation === entryGeneration) errors[id] = e instanceof Error ? e.message : String(e);
132
+ } finally {
133
+ if (generation === entryGeneration) loading[id] = false;
134
+ }
135
+ }
136
+
137
+ watch(
138
+ () => [props.modelValue, props.entry?.cacheKey],
139
+ ([open]) => {
140
+ entryGeneration += 1;
141
+ if (!open) return;
142
+ // Reset per-entry state when a new entry is shown.
143
+ for (const t of TABS) {
144
+ loading[t.id] = false;
145
+ contents[t.id] = null;
146
+ errors[t.id] = null;
147
+ fetched[t.id] = false;
148
+ }
149
+ activeTab.value = "parameter_model";
150
+ void loadTab("parameter_model");
151
+ },
152
+ );
153
+
154
+ watch(activeTab, (id) => {
155
+ void loadTab(id);
156
+ });
157
+
158
+ async function copy() {
159
+ await navigator.clipboard.writeText(activePretty.value);
160
+ toast.add({ severity: "success", summary: "Copied", life: 1500 });
161
+ }
162
+ </script>
163
+
164
+ <style scoped>
165
+ .source-note {
166
+ color: var(--p-text-muted-color);
167
+ font-size: var(--gx-fs-xs);
168
+ }
169
+
170
+ .raw-json {
171
+ max-height: 60vh;
172
+ overflow: auto;
173
+ margin: 0;
174
+ padding: var(--gx-sp-3);
175
+ background: var(--p-surface-100, #f3f4f6);
176
+ color: var(--p-text-color, inherit);
177
+ border-radius: 4px;
178
+ font-family: var(--gx-mono);
179
+ font-size: var(--gx-fs-xs);
180
+ white-space: pre;
181
+ }
182
+
183
+ :global(.dark) .raw-json {
184
+ background: var(--p-surface-800, #1f2937);
185
+ }
186
+
187
+ .loading-state,
188
+ .error-state {
189
+ padding: var(--gx-sp-4);
190
+ text-align: center;
191
+ }
192
+
193
+ .error-state {
194
+ color: var(--p-red-500, #ef4444);
195
+ }
196
+ </style>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <div class="cache-stats" data-description="tool cache stats">
3
+ <div class="stat-line">
4
+ <strong>{{ stats.count }}</strong> tools
5
+ <template v-if="stats.totalBytes !== undefined">
6
+ · <strong>{{ formatBytes(stats.totalBytes) }}</strong>
7
+ </template>
8
+ <template v-for="(n, source) in stats.bySource" :key="source">
9
+ · <strong>{{ n }}</strong> {{ source }}
10
+ </template>
11
+ </div>
12
+ <div v-if="stats.oldest || stats.newest" class="stat-line stat-meta">
13
+ <span v-if="stats.oldest">Oldest: {{ formatDate(stats.oldest) }}</span>
14
+ <span v-if="stats.newest">Newest: {{ formatDate(stats.newest) }}</span>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import type { CacheStats } from "../client.js";
21
+
22
+ defineProps<{ stats: CacheStats }>();
23
+
24
+ function formatBytes(n: number): string {
25
+ if (n < 1024) return `${n} B`;
26
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
27
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
28
+ }
29
+
30
+ function formatDate(iso: string): string {
31
+ return new Date(iso).toLocaleString();
32
+ }
33
+ </script>
34
+
35
+ <style scoped>
36
+ .cache-stats {
37
+ padding: var(--gx-sp-3) var(--gx-sp-4);
38
+ background: var(--p-content-background, #fff);
39
+ border: 1px solid var(--p-content-border-color, #e5e7eb);
40
+ border-radius: 6px;
41
+ margin-bottom: var(--gx-sp-4);
42
+ }
43
+
44
+ .stat-line {
45
+ font-size: var(--gx-fs-sm);
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ gap: 0.5em;
49
+ align-items: baseline;
50
+ }
51
+
52
+ .stat-meta {
53
+ margin-top: var(--gx-sp-2);
54
+ gap: var(--gx-sp-4);
55
+ color: var(--p-text-muted-color, #6b7280);
56
+ font-size: var(--gx-fs-xs);
57
+ }
58
+ </style>
@@ -0,0 +1,238 @@
1
+ <template>
2
+ <div class="cache-table">
3
+ <div class="list-toolbar">
4
+ <IconField class="search-field">
5
+ <InputIcon class="pi pi-search" />
6
+ <InputText v-model="filter" placeholder="Filter by tool id…" size="small" />
7
+ </IconField>
8
+ <Select
9
+ v-model="sourceFilter"
10
+ :options="sourceOptions"
11
+ placeholder="All sources"
12
+ showClear
13
+ size="small"
14
+ class="source-select"
15
+ />
16
+ <label class="undecodable-toggle">
17
+ <Checkbox v-model="undecodableOnly" :binary="true" />
18
+ <span>Undecodable only</span>
19
+ </label>
20
+ <span class="count">
21
+ {{ filtered.length }}{{ filtered.length !== entries.length ? ` of ${entries.length}` : "" }}
22
+ entries
23
+ </span>
24
+ </div>
25
+
26
+ <DataTable
27
+ :value="filtered"
28
+ :loading="loading"
29
+ dataKey="cacheKey"
30
+ :rowHover="true"
31
+ stripedRows
32
+ sortField="cachedAt"
33
+ :sortOrder="-1"
34
+ :paginator="filtered.length > 25"
35
+ :rows="25"
36
+ :rowsPerPageOptions="[25, 50, 100]"
37
+ >
38
+ <template #empty>
39
+ <div class="empty-state">
40
+ <i class="pi pi-database empty-icon" />
41
+ <p v-if="filter || sourceFilter || undecodableOnly">
42
+ No entries match the current filters.
43
+ </p>
44
+ <p v-else>The tool cache is empty.</p>
45
+ </div>
46
+ </template>
47
+
48
+ <Column field="toolId" header="Tool" sortable>
49
+ <template #body="{ data: row }">
50
+ <div class="tool-cell">
51
+ <i
52
+ v-if="!(row as Entry).decodable"
53
+ class="pi pi-exclamation-triangle warn-icon"
54
+ v-tooltip.right="'Cached payload does not decode as a ParsedTool'"
55
+ />
56
+ <span class="tool-id">{{ (row as Entry).toolId }}</span>
57
+ </div>
58
+ </template>
59
+ </Column>
60
+ <Column field="toolVersion" header="Version" sortable style="width: 10rem" />
61
+ <Column field="source" header="Source" sortable style="width: 7rem">
62
+ <template #body="{ data: row }">
63
+ <Tag :value="(row as Entry).source" severity="secondary" />
64
+ </template>
65
+ </Column>
66
+ <Column field="sizeBytes" header="Size" sortable style="width: 7rem">
67
+ <template #body="{ data: row }">
68
+ <span v-if="(row as Entry).sizeBytes !== undefined">
69
+ {{ formatBytes((row as Entry).sizeBytes!) }}
70
+ </span>
71
+ <span v-else class="muted">—</span>
72
+ </template>
73
+ </Column>
74
+ <Column field="cachedAt" header="Cached" sortable style="width: 11rem">
75
+ <template #body="{ data: row }">
76
+ <span :title="(row as Entry).cachedAt">{{
77
+ formatRelative((row as Entry).cachedAt)
78
+ }}</span>
79
+ </template>
80
+ </Column>
81
+ <Column header="" style="width: 11rem; text-align: right">
82
+ <template #body="{ data: row }">
83
+ <Button
84
+ icon="pi pi-eye"
85
+ aria-label="View tool definition"
86
+ text
87
+ size="small"
88
+ @click="emit('view', row as Entry)"
89
+ v-tooltip.left="'View tool definition'"
90
+ />
91
+ <Button
92
+ icon="pi pi-refresh"
93
+ text
94
+ size="small"
95
+ :disabled="!(row as Entry).refetchable"
96
+ @click="emit('refetch', row as Entry)"
97
+ v-tooltip.left="
98
+ (row as Entry).refetchable
99
+ ? 'Re-fetch from source'
100
+ : 'Cannot re-fetch — tool id is unknown/orphan'
101
+ "
102
+ />
103
+ <a
104
+ v-if="(row as Entry).toolshedUrl"
105
+ :href="(row as Entry).toolshedUrl"
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ >
109
+ <Button icon="pi pi-external-link" text size="small" v-tooltip.left="'Open ToolShed'" />
110
+ </a>
111
+ <Button
112
+ icon="pi pi-trash"
113
+ text
114
+ size="small"
115
+ severity="danger"
116
+ @click="emit('delete', row as Entry)"
117
+ v-tooltip.left="'Delete'"
118
+ />
119
+ </template>
120
+ </Column>
121
+ </DataTable>
122
+ </div>
123
+ </template>
124
+
125
+ <script setup lang="ts">
126
+ import { computed, ref } from "vue";
127
+ import Button from "primevue/button";
128
+ import Checkbox from "primevue/checkbox";
129
+ import Column from "primevue/column";
130
+ import DataTable from "primevue/datatable";
131
+ import IconField from "primevue/iconfield";
132
+ import InputIcon from "primevue/inputicon";
133
+ import InputText from "primevue/inputtext";
134
+ import Select from "primevue/select";
135
+ import Tag from "primevue/tag";
136
+ import type { CachedToolEntry as Entry } from "../client.js";
137
+
138
+ const props = defineProps<{ entries: Entry[]; loading?: boolean }>();
139
+ const emit = defineEmits<{
140
+ view: [entry: Entry];
141
+ refetch: [entry: Entry];
142
+ delete: [entry: Entry];
143
+ }>();
144
+
145
+ const filter = ref("");
146
+ const sourceFilter = ref<string | null>(null);
147
+ const undecodableOnly = ref(false);
148
+
149
+ const sourceOptions = computed(() =>
150
+ Array.from(new Set(props.entries.map((e) => e.source))).sort(),
151
+ );
152
+
153
+ const filtered = computed(() => {
154
+ const q = filter.value.toLowerCase();
155
+ return props.entries.filter((e) => {
156
+ if (q && !e.toolId.toLowerCase().includes(q)) return false;
157
+ if (sourceFilter.value && e.source !== sourceFilter.value) return false;
158
+ if (undecodableOnly.value && e.decodable) return false;
159
+ return true;
160
+ });
161
+ });
162
+
163
+ function formatBytes(n: number): string {
164
+ if (n < 1024) return `${n} B`;
165
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
166
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
167
+ }
168
+
169
+ function formatRelative(iso: string): string {
170
+ const ms = Date.now() - new Date(iso).getTime();
171
+ const s = Math.round(ms / 1000);
172
+ if (s < 60) return `${s}s ago`;
173
+ const m = Math.round(s / 60);
174
+ if (m < 60) return `${m}m ago`;
175
+ const h = Math.round(m / 60);
176
+ if (h < 48) return `${h}h ago`;
177
+ const d = Math.round(h / 24);
178
+ return `${d}d ago`;
179
+ }
180
+ </script>
181
+
182
+ <style scoped>
183
+ .list-toolbar {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: var(--gx-sp-3);
187
+ margin-bottom: var(--gx-sp-3);
188
+ }
189
+
190
+ .source-select {
191
+ min-width: 10rem;
192
+ }
193
+
194
+ .undecodable-toggle {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 0.4em;
198
+ font-size: var(--gx-fs-sm);
199
+ cursor: pointer;
200
+ }
201
+
202
+ .count {
203
+ margin-left: auto;
204
+ font-size: var(--gx-fs-sm);
205
+ color: var(--p-text-muted-color, #6b7280);
206
+ }
207
+
208
+ .tool-cell {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ gap: 0.4em;
212
+ }
213
+
214
+ .tool-id {
215
+ font-family: var(--gx-mono);
216
+ }
217
+
218
+ .warn-icon {
219
+ color: var(--p-orange-500, #f59e0b);
220
+ }
221
+
222
+ .muted {
223
+ color: var(--p-text-muted-color, #6b7280);
224
+ }
225
+
226
+ .empty-state {
227
+ text-align: center;
228
+ padding: var(--gx-sp-6) var(--gx-sp-4);
229
+ color: var(--p-text-muted-color, #6b7280);
230
+ }
231
+
232
+ .empty-icon {
233
+ font-size: 2rem;
234
+ display: block;
235
+ margin-bottom: var(--gx-sp-2);
236
+ opacity: 0.5;
237
+ }
238
+ </style>
@@ -0,0 +1,152 @@
1
+ import { ref } from "vue";
2
+ import type { CacheClient, CachedToolEntry, CacheStats } from "../client.js";
3
+
4
+ /** Pull a server-side error message out of a router-style `{detail: "..."}` body, with fallbacks. */
5
+ function detailOf(err: unknown, fallback: string): string {
6
+ if (err && typeof err === "object" && "detail" in err) {
7
+ const d = (err as { detail: unknown }).detail;
8
+ if (typeof d === "string" && d.length > 0) return d;
9
+ }
10
+ return fallback;
11
+ }
12
+
13
+ export function useToolCache(client: CacheClient) {
14
+ const entries = ref<CachedToolEntry[]>([]);
15
+ const stats = ref<CacheStats>({ count: 0, bySource: {} });
16
+ const loading = ref(false);
17
+ const error = ref<string | null>(null);
18
+
19
+ async function refresh(opts: { decode?: boolean } = {}) {
20
+ loading.value = true;
21
+ error.value = null;
22
+ try {
23
+ const { data, error: err } = await client.GET("/api/tool-cache", {
24
+ params: { query: opts.decode ? { decode: "1" } : {} },
25
+ });
26
+ if (err) {
27
+ error.value = detailOf(err, "Failed to load tool cache");
28
+ } else if (data) {
29
+ entries.value = data.entries;
30
+ stats.value = data.stats;
31
+ }
32
+ } finally {
33
+ loading.value = false;
34
+ }
35
+ }
36
+
37
+ async function loadParameterModel(toolId: string, toolVersion: string) {
38
+ const { data, error: err } = await client.GET("/api/tools/{tool_id}/versions/{tool_version}", {
39
+ params: { path: { tool_id: toolId, tool_version: toolVersion } },
40
+ });
41
+ if (err) throw new Error(detailOf(err, "Failed to load parameter model"));
42
+ return data;
43
+ }
44
+
45
+ async function loadParameterRequestSchema(toolId: string, toolVersion: string) {
46
+ const { data, error: err } = await client.GET(
47
+ "/api/tools/{tool_id}/versions/{tool_version}/parameter_request_schema",
48
+ { params: { path: { tool_id: toolId, tool_version: toolVersion } } },
49
+ );
50
+ if (err) throw new Error(detailOf(err, "Failed to load request schema"));
51
+ return data;
52
+ }
53
+
54
+ async function loadParameterLandingRequestSchema(toolId: string, toolVersion: string) {
55
+ const { data, error: err } = await client.GET(
56
+ "/api/tools/{tool_id}/versions/{tool_version}/parameter_landing_request_schema",
57
+ { params: { path: { tool_id: toolId, tool_version: toolVersion } } },
58
+ );
59
+ if (err) throw new Error(detailOf(err, "Failed to load landing-request schema"));
60
+ return data;
61
+ }
62
+
63
+ async function loadParameterTestCaseXmlSchema(toolId: string, toolVersion: string) {
64
+ const { data, error: err } = await client.GET(
65
+ "/api/tools/{tool_id}/versions/{tool_version}/parameter_test_case_xml_schema",
66
+ { params: { path: { tool_id: toolId, tool_version: toolVersion } } },
67
+ );
68
+ if (err) throw new Error(detailOf(err, "Failed to load test-case-XML schema"));
69
+ return data;
70
+ }
71
+
72
+ async function loadToolSource(toolId: string, toolVersion: string) {
73
+ const { data, error: err } = await client.GET(
74
+ "/api/tools/{tool_id}/versions/{tool_version}/tool_source",
75
+ { params: { path: { tool_id: toolId, tool_version: toolVersion } }, parseAs: "text" },
76
+ );
77
+ if (err) throw new Error(detailOf(err, "Failed to load tool source"));
78
+ return data;
79
+ }
80
+
81
+ async function del(cacheKey: string) {
82
+ error.value = null;
83
+ const { error: err } = await client.DELETE("/api/tool-cache/{cacheKey}", {
84
+ params: { path: { cacheKey } },
85
+ });
86
+ if (err) {
87
+ const msg = detailOf(err, "Delete failed");
88
+ error.value = msg;
89
+ throw new Error(msg);
90
+ }
91
+ await refresh();
92
+ }
93
+
94
+ async function clear(prefix?: string) {
95
+ error.value = null;
96
+ const { data, error: err } = await client.DELETE("/api/tool-cache", {
97
+ params: { query: prefix ? { prefix } : {} },
98
+ });
99
+ if (err) {
100
+ const msg = detailOf(err, "Clear failed");
101
+ error.value = msg;
102
+ throw new Error(msg);
103
+ }
104
+ await refresh();
105
+ return data;
106
+ }
107
+
108
+ async function refetch(toolId: string, toolVersion?: string) {
109
+ error.value = null;
110
+ const { data, error: err } = await client.POST("/api/tool-cache/refetch", {
111
+ body: { toolId, ...(toolVersion ? { toolVersion } : {}) },
112
+ });
113
+ if (err) {
114
+ const msg = detailOf(err, "Refetch failed");
115
+ error.value = msg;
116
+ throw new Error(msg);
117
+ }
118
+ await refresh();
119
+ return data;
120
+ }
121
+
122
+ async function add(toolId: string, toolVersion?: string) {
123
+ error.value = null;
124
+ const { data, error: err } = await client.POST("/api/tool-cache/add", {
125
+ body: { toolId, ...(toolVersion ? { toolVersion } : {}) },
126
+ });
127
+ if (err) {
128
+ const msg = detailOf(err, "Add failed");
129
+ error.value = msg;
130
+ throw new Error(msg);
131
+ }
132
+ await refresh();
133
+ return data;
134
+ }
135
+
136
+ return {
137
+ entries,
138
+ stats,
139
+ loading,
140
+ error,
141
+ refresh,
142
+ loadToolSource,
143
+ loadParameterModel,
144
+ loadParameterRequestSchema,
145
+ loadParameterLandingRequestSchema,
146
+ loadParameterTestCaseXmlSchema,
147
+ del,
148
+ clear,
149
+ refetch,
150
+ add,
151
+ };
152
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @module @galaxy-tool-util/cache-ui
3
+ *
4
+ * Vue 3 + PrimeVue components and composable for inspecting and managing the
5
+ * Galaxy tool cache exposed by `gxwf-web` and `tool-cache-proxy`. Both servers
6
+ * implement the same cache HTTP surface; pass a `createCacheClient(baseUrl)`
7
+ * to the components and they work against either.
8
+ */
9
+
10
+ export { default as ToolCacheView } from "./views/ToolCacheView.vue";
11
+ export { default as ToolCacheTable } from "./components/ToolCacheTable.vue";
12
+ export { default as ToolCacheStats } from "./components/ToolCacheStats.vue";
13
+ export { default as ToolCacheRawDialog } from "./components/ToolCacheRawDialog.vue";
14
+ export { useToolCache } from "./composables/useToolCache.js";
15
+ export { createCacheClient } from "./client.js";
16
+ export type { CacheClient, CachedToolEntry, CacheStats } from "./client.js";
@@ -0,0 +1,314 @@
1
+ <template>
2
+ <div>
3
+ <div class="view-header">
4
+ <div>
5
+ <h1>Tool Cache</h1>
6
+ <p class="subtitle">Inspect and manage the in-memory + on-disk parsed-tool cache.</p>
7
+ </div>
8
+ <div class="header-actions">
9
+ <label
10
+ class="decode-toggle"
11
+ v-tooltip.bottom="'Probe each entry for decode-ability (slower)'"
12
+ >
13
+ <Checkbox v-model="decodeProbe" :binary="true" @change="reload" />
14
+ <span>Decode probe</span>
15
+ </label>
16
+ <Button label="Refresh" icon="pi pi-refresh" text :loading="loading" @click="reload" />
17
+ <Button
18
+ icon="pi pi-ellipsis-v"
19
+ text
20
+ aria-label="Cache actions"
21
+ @click="(e) => menu?.toggle(e)"
22
+ />
23
+ <Menu ref="menu" :model="menuItems" :popup="true" />
24
+ </div>
25
+ </div>
26
+
27
+ <Message v-if="error" severity="error" :closable="false">{{ error }}</Message>
28
+
29
+ <ToolCacheStats :stats="stats" />
30
+
31
+ <ToolCacheTable
32
+ :entries="entries"
33
+ :loading="loading"
34
+ @view="viewEntry"
35
+ @refetch="refetchEntry"
36
+ @delete="confirmDelete"
37
+ />
38
+
39
+ <ToolCacheRawDialog v-model="rawOpen" :entry="rawEntry" :loaders="dialogLoaders" />
40
+
41
+ <Dialog v-model:visible="addOpen" header="Add tool to cache" modal :style="{ width: '24rem' }">
42
+ <div class="add-form">
43
+ <label>
44
+ <span>Tool ID</span>
45
+ <InputText
46
+ v-model="addToolId"
47
+ placeholder="toolshed.g2.../repos/owner/repo/tool/version"
48
+ />
49
+ </label>
50
+ <label>
51
+ <span>Version (optional)</span>
52
+ <InputText v-model="addToolVersion" />
53
+ </label>
54
+ </div>
55
+ <template #footer>
56
+ <Button label="Cancel" text @click="addOpen = false" />
57
+ <Button label="Add" icon="pi pi-plus" :loading="addPending" @click="doAdd" />
58
+ </template>
59
+ </Dialog>
60
+
61
+ <Dialog
62
+ v-model:visible="prefixOpen"
63
+ header="Clear by tool-id prefix"
64
+ modal
65
+ :style="{ width: '24rem' }"
66
+ >
67
+ <div class="add-form">
68
+ <label>
69
+ <span>Tool ID prefix</span>
70
+ <InputText v-model="prefixValue" placeholder="e.g. toolshed.g2.bx.psu.edu/repos/iuc/" />
71
+ </label>
72
+ </div>
73
+ <template #footer>
74
+ <Button label="Cancel" text @click="prefixOpen = false" />
75
+ <Button
76
+ label="Clear"
77
+ icon="pi pi-trash"
78
+ severity="danger"
79
+ :loading="clearPending"
80
+ @click="doClearPrefix"
81
+ />
82
+ </template>
83
+ </Dialog>
84
+
85
+ <ConfirmDialog />
86
+ </div>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import { onMounted, ref } from "vue";
91
+ import Button from "primevue/button";
92
+ import Checkbox from "primevue/checkbox";
93
+ import ConfirmDialog from "primevue/confirmdialog";
94
+ import Dialog from "primevue/dialog";
95
+ import InputText from "primevue/inputtext";
96
+ import Menu from "primevue/menu";
97
+ import Message from "primevue/message";
98
+ import { useConfirm } from "primevue/useconfirm";
99
+ import { useToast } from "primevue/usetoast";
100
+ import ToolCacheStats from "../components/ToolCacheStats.vue";
101
+ import ToolCacheTable from "../components/ToolCacheTable.vue";
102
+ import ToolCacheRawDialog from "../components/ToolCacheRawDialog.vue";
103
+ import { useToolCache } from "../composables/useToolCache.js";
104
+ import type { CachedToolEntry as Entry, CacheClient } from "../client.js";
105
+
106
+ const props = defineProps<{ client: CacheClient }>();
107
+
108
+ const {
109
+ entries,
110
+ stats,
111
+ loading,
112
+ error,
113
+ refresh,
114
+ loadToolSource,
115
+ loadParameterModel,
116
+ loadParameterRequestSchema,
117
+ loadParameterLandingRequestSchema,
118
+ loadParameterTestCaseXmlSchema,
119
+ del,
120
+ clear,
121
+ refetch,
122
+ add,
123
+ } = useToolCache(props.client);
124
+ const dialogLoaders = {
125
+ tool_source: loadToolSource,
126
+ parameter_model: loadParameterModel,
127
+ parameter_request_schema: loadParameterRequestSchema,
128
+ parameter_landing_request_schema: loadParameterLandingRequestSchema,
129
+ parameter_test_case_xml_schema: loadParameterTestCaseXmlSchema,
130
+ };
131
+ const toast = useToast();
132
+ const confirm = useConfirm();
133
+
134
+ const menu = ref<InstanceType<typeof Menu> | null>(null);
135
+ const rawOpen = ref(false);
136
+ const rawEntry = ref<Entry | null>(null);
137
+ const decodeProbe = ref(false);
138
+
139
+ function reload() {
140
+ void refresh({ decode: decodeProbe.value });
141
+ }
142
+
143
+ function errMsg(e: unknown): string {
144
+ return e instanceof Error ? e.message : String(e);
145
+ }
146
+
147
+ const addOpen = ref(false);
148
+ const addToolId = ref("");
149
+ const addToolVersion = ref("");
150
+ const addPending = ref(false);
151
+
152
+ const prefixOpen = ref(false);
153
+ const prefixValue = ref("");
154
+ const clearPending = ref(false);
155
+
156
+ const menuItems = [
157
+ { label: "Add tool…", icon: "pi pi-plus", command: () => (addOpen.value = true) },
158
+ {
159
+ label: "Clear by prefix…",
160
+ icon: "pi pi-filter-slash",
161
+ command: () => (prefixOpen.value = true),
162
+ },
163
+ { separator: true },
164
+ {
165
+ label: "Clear all",
166
+ icon: "pi pi-trash",
167
+ command: () => {
168
+ confirm.require({
169
+ message: `Remove all ${stats.value.count} cached tools?`,
170
+ header: "Clear cache",
171
+ icon: "pi pi-exclamation-triangle",
172
+ acceptProps: { severity: "danger", label: "Clear all" },
173
+ accept: async () => {
174
+ try {
175
+ const data = await clear();
176
+ toast.add({
177
+ severity: "success",
178
+ summary: `Removed ${data?.removed ?? 0} entries`,
179
+ life: 2500,
180
+ });
181
+ } catch (e) {
182
+ toast.add({ severity: "error", summary: errMsg(e), life: 4000 });
183
+ }
184
+ },
185
+ });
186
+ },
187
+ },
188
+ ];
189
+
190
+ function viewEntry(e: Entry) {
191
+ rawEntry.value = e;
192
+ rawOpen.value = true;
193
+ }
194
+
195
+ function confirmDelete(e: Entry) {
196
+ confirm.require({
197
+ message: `Delete cached entry for ${e.toolId} ${e.toolVersion}?`,
198
+ header: "Delete entry",
199
+ icon: "pi pi-exclamation-triangle",
200
+ acceptProps: { severity: "danger", label: "Delete" },
201
+ accept: async () => {
202
+ try {
203
+ await del(e.cacheKey);
204
+ toast.add({ severity: "success", summary: `Deleted ${e.toolId}`, life: 2000 });
205
+ } catch (err) {
206
+ toast.add({ severity: "error", summary: errMsg(err), life: 4000 });
207
+ }
208
+ },
209
+ });
210
+ }
211
+
212
+ async function refetchEntry(e: Entry) {
213
+ try {
214
+ await refetch(e.toolId, e.requestVersion ?? e.toolVersion);
215
+ toast.add({
216
+ severity: "success",
217
+ summary: `Re-fetched ${e.toolId} ${e.toolVersion}`,
218
+ life: 2500,
219
+ });
220
+ } catch (err) {
221
+ toast.add({ severity: "error", summary: `Failed: ${errMsg(err)}`, life: 4000 });
222
+ }
223
+ }
224
+
225
+ async function doAdd() {
226
+ if (!addToolId.value) return;
227
+ addPending.value = true;
228
+ try {
229
+ const data = await add(addToolId.value, addToolVersion.value || undefined);
230
+ toast.add({
231
+ severity: "success",
232
+ summary: data?.alreadyCached ? "Already cached" : `Added ${addToolId.value}`,
233
+ life: 2500,
234
+ });
235
+ addOpen.value = false;
236
+ addToolId.value = "";
237
+ addToolVersion.value = "";
238
+ } catch (err) {
239
+ toast.add({ severity: "error", summary: `Failed: ${errMsg(err)}`, life: 4000 });
240
+ } finally {
241
+ addPending.value = false;
242
+ }
243
+ }
244
+
245
+ async function doClearPrefix() {
246
+ if (!prefixValue.value) return;
247
+ clearPending.value = true;
248
+ try {
249
+ const data = await clear(prefixValue.value);
250
+ toast.add({
251
+ severity: "success",
252
+ summary: `Removed ${data?.removed ?? 0} entries`,
253
+ life: 2500,
254
+ });
255
+ prefixOpen.value = false;
256
+ prefixValue.value = "";
257
+ } catch (err) {
258
+ toast.add({ severity: "error", summary: errMsg(err), life: 4000 });
259
+ } finally {
260
+ clearPending.value = false;
261
+ }
262
+ }
263
+
264
+ onMounted(reload);
265
+ </script>
266
+
267
+ <style scoped>
268
+ .view-header {
269
+ display: flex;
270
+ align-items: flex-start;
271
+ justify-content: space-between;
272
+ gap: var(--gx-sp-4);
273
+ margin-bottom: var(--gx-sp-4);
274
+ }
275
+
276
+ .view-header h1 {
277
+ margin: 0 0 var(--gx-sp-1);
278
+ font-size: var(--gx-fs-xl);
279
+ }
280
+
281
+ .subtitle {
282
+ margin: 0;
283
+ color: var(--p-text-muted-color, #6b7280);
284
+ font-size: var(--gx-fs-sm);
285
+ }
286
+
287
+ .header-actions {
288
+ display: flex;
289
+ gap: var(--gx-sp-2);
290
+ align-items: center;
291
+ }
292
+
293
+ .decode-toggle {
294
+ display: inline-flex;
295
+ align-items: center;
296
+ gap: 0.4em;
297
+ font-size: var(--gx-fs-sm);
298
+ color: var(--p-text-muted-color, #6b7280);
299
+ cursor: pointer;
300
+ }
301
+
302
+ .add-form {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: var(--gx-sp-3);
306
+ }
307
+
308
+ .add-form label {
309
+ display: flex;
310
+ flex-direction: column;
311
+ gap: 0.3em;
312
+ font-size: var(--gx-fs-sm);
313
+ }
314
+ </style>
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- Bootstrap package for @galaxy-tool-util/cache-ui.
2
-
3
- This stub establishes the npm package so its GitHub Actions trusted publisher can be configured. The full package is published by the repository release workflow.