diff --git a/app/assets/css/table.css b/app/assets/css/table.css index 8ee47c8c..8557a462 100644 --- a/app/assets/css/table.css +++ b/app/assets/css/table.css @@ -29,15 +29,19 @@ } .structured-table { - th:not(:first-child) .p-datatable-column-header-content { - justify-content: center; + th:not(:first-child) { + text-align: center; - .p-datatable-popover-filter { - margin-inline-start: 0.2rem; - } + .p-datatable-column-header-content { + justify-content: center; + + .p-datatable-popover-filter { + margin-inline-start: 0.2rem; + } - .p-button-icon-only { - width: 1.5rem; + .p-button-icon-only { + width: 1.5rem; + } } } diff --git a/app/components/analysis/AnalysesTable.vue b/app/components/analysis/AnalysesTable.vue index bb7e8d1f..216c1c7b 100644 --- a/app/components/analysis/AnalysesTable.vue +++ b/app/components/analysis/AnalysesTable.vue @@ -27,6 +27,7 @@ import { type Route, } from "~/services/Api"; import { ApprovalStatus } from "~/types/node"; +import { parseKongTags } from "~/utils/parse-kong-tags"; import ContainerCounter from "~/components/analysis/ContainerCounter.vue"; import { useDatastoreRequirement } from "~/composables/useDatastoreRequirement"; import type { @@ -162,11 +163,9 @@ async function getKongRoutes() { .catch(() => undefined)) as ListRoutes; if (kongRoutesResp && kongRoutesResp.data) { const projIds: string[] = []; - kongRoutesResp.data.forEach((proj: Route) => { - const nameChunks = proj.name?.split("-"); - if (nameChunks && nameChunks.length > 1) { - nameChunks.pop(); // Remove suffix, either "fhir" or "s3" - const projUuid = nameChunks.join("-"); + kongRoutesResp.data.forEach((route: Route) => { + const projUuid = parseKongTags(route.tags).project; + if (projUuid) { projIds.push(projUuid); } }); @@ -686,7 +685,6 @@ const onCloseNavToast = () => { :showFilterOperator="false" field="execution_status" filterField="execution_status" - headerStyle="text-align: center" > diff --git a/app/components/data-stores/DataStoreList.vue b/app/components/data-stores/DataStoreList.vue index 78b46689..d5783bc7 100644 --- a/app/components/data-stores/DataStoreList.vue +++ b/app/components/data-stores/DataStoreList.vue @@ -5,8 +5,8 @@ import DetailedDataStoreTable from "~/components/data-stores/DetailedDataStoreTa const { dataStores, projectNameMap, loading, refresh } = await useDataStoreList(); -function onDeleteDataStore(dsName: string) { - dataStores.value = dataStores.value.filter((store) => store.name !== dsName); +function onDeleteDataStore(dsId: string) { + dataStores.value = dataStores.value.filter((store) => store.id !== dsId); refresh(); } diff --git a/app/components/data-stores/DetailedDataStoreTable.vue b/app/components/data-stores/DetailedDataStoreTable.vue index 201c7c82..1a465184 100644 --- a/app/components/data-stores/DetailedDataStoreTable.vue +++ b/app/components/data-stores/DetailedDataStoreTable.vue @@ -6,22 +6,28 @@ import { useToast } from "primevue/usetoast"; import type { Route } from "~/services/Api"; import { FilterMatchMode } from "@primevue/core/api"; import SearchBar from "~/components/table/SearchBar.vue"; -import { extractUuid } from "~/utils/extract-uuid-from-kong-username"; +import { parseKongTags } from "~/utils/parse-kong-tags"; import { parseUnixTimestamp } from "~/utils/format-data-row"; import { getDataStoreTypeSeverity } from "~/utils/status-tag-severity"; -import type { ModifiedDetailedService } from "~/services/modifiedApiInterfaces"; +import type { + ModifiedDetailedService, + modifiedTimestamp, +} from "~/services/modifiedApiInterfaces"; import { useNuxtApp } from "nuxt/app"; interface DetailedDataStoreTableRow { - name?: string | undefined; + rowKey: string; + datastoreId?: string | undefined; + projectId?: string | undefined; + name?: string | null | undefined; type?: string | undefined; project?: string | undefined; - path?: string | undefined; - host?: string | undefined; - port?: number | undefined; - protocol?: string | undefined; - created_at?: string | undefined; - updated_at?: string | undefined; + path?: string | null | undefined; + host?: string | null | undefined; + port?: number | null | undefined; + protocol?: string | null | undefined; + created_at?: modifiedTimestamp | undefined; + updated_at?: modifiedTimestamp | undefined; } const props = defineProps({ @@ -44,31 +50,42 @@ const dataStores = computed(() => { if (props.stores && props.stores.length > 0) { props.stores.forEach((store: ModifiedDetailedService) => { - const formattedRow: DetailedDataStoreTableRow = parseUnixTimestamp( - store, - ["created_at", "updated_at"], - ); + const formattedRow = parseUnixTimestamp(store, [ + "created_at", + "updated_at", + ]) as ModifiedDetailedService; + const baseRow: DetailedDataStoreTableRow = { + rowKey: store.id!, + datastoreId: store.id ?? undefined, + name: store.name, + type: parseKongTags(store.tags).type, + project: "N/A", + path: store.path, + host: store.host, + port: store.port, + protocol: store.protocol, + created_at: formattedRow.created_at, + updated_at: formattedRow.updated_at, + }; const routes = store.routes; if (routes && routes.length > 0) { - routes.forEach((proj: Route) => { - const projectParts = extractUuid(proj.name!); - const dataStoreType = projectParts[0]; - const projectUuid = projectParts[1] as string; - const newRow: DetailedDataStoreTableRow = { - name: store.name, - type: dataStoreType, - project: props.projectNameMap?.has(projectUuid) - ? props.projectNameMap.get(projectUuid)! - : "N/A", - path: store.path, - host: store.host, - port: store.port, - protocol: store.protocol, - created_at: formattedRow.created_at, - updated_at: formattedRow.updated_at, - }; - tableRows.push(newRow); + routes.forEach((route: Route) => { + const routeTags = parseKongTags(route.tags); + const projectUuid = routeTags.project ?? route["projectId"]; + tableRows.push({ + ...baseRow, + rowKey: `${store.id}:${route.id}`, + projectId: projectUuid, + type: routeTags.type ?? baseRow.type, + project: + projectUuid && props.projectNameMap?.has(projectUuid) + ? props.projectNameMap.get(projectUuid)! + : "N/A", + }); }); + } else { + // Data stores can now exist without any linked project + tableRows.push(baseRow); } }); } @@ -76,9 +93,11 @@ const dataStores = computed(() => { return tableRows; }); -async function onConfirmDeleteDataStore(dsName: string) { - deleteLoadingFor.value = dsName; - const { status } = await deleteDataStore(dsName); +async function onConfirmDeleteDataStore(row: DetailedDataStoreTableRow) { + const dsId = row.datastoreId ?? row.name!; + deleteLoadingFor.value = dsId; + // Cascade removes the project links (routes) along with the store + const { status } = await deleteDataStore(dsId, true); if (status.value === "success") { toast.add({ severity: "success", @@ -86,7 +105,7 @@ async function onConfirmDeleteDataStore(dsName: string) { detail: "The data store was successfully deleted", life: 3000, }); - emit("deleteDataStore", dsName); + emit("deleteDataStore", dsId); } else { toast.add({ severity: "error", @@ -98,7 +117,7 @@ async function onConfirmDeleteDataStore(dsName: string) { deleteLoadingFor.value = null; } -const confirmDelete = (dsName: string) => { +const confirmDelete = (row: DetailedDataStoreTableRow) => { confirm.require({ header: "Confirm Data Store Removal", message: @@ -115,20 +134,22 @@ const confirmDelete = (dsName: string) => { icon: "pi pi-times", }, accept: () => { - onConfirmDeleteDataStore(dsName); + onConfirmDeleteDataStore(row); }, reject: () => {}, }); }; -async function onCheckConnection(dsName: string) { - checkingConnectionFor.value = dsName; - const [dsType, projectId] = extractUuid(dsName); +async function onCheckConnection(row: DetailedDataStoreTableRow) { + checkingConnectionFor.value = row.rowKey; const connStatus = await useNuxtApp() - .$hubApi(`/kong/project/${projectId}/${dsType}/health`, { - method: "GET", - }) + .$hubApi( + `/kong/project/${row.projectId}/datastore/${row.datastoreId}/health`, + { + method: "GET", + }, + ) .catch(() => { toast.add({ severity: "error", @@ -166,10 +187,12 @@ function resetFilters() { }; clearedFilters[filterKey].value = undefined; } + // @ts-expect-error filters are fine filters.value = clearedFilters; } const updateFilters = (filterText: string) => { + // @ts-expect-error filters are fine filters.value.global.value = filterText; }; @@ -285,11 +308,17 @@ const updateFilters = (filterText: string) => { @@ -303,10 +332,11 @@ const updateFilters = (filterText: string) => { diff --git a/app/components/data-stores/create/DataStoreProjectInitializer.vue b/app/components/data-stores/create/DataStoreProjectInitializer.vue index 5252f144..6453a74b 100644 --- a/app/components/data-stores/create/DataStoreProjectInitializer.vue +++ b/app/components/data-stores/create/DataStoreProjectInitializer.vue @@ -9,10 +9,21 @@ import InputNumber from "primevue/inputnumber"; import InputGroupAddon from "primevue/inputgroupaddon"; import InputGroup from "primevue/inputgroup"; import DataStoreHelpBox from "~/components/data-stores/create/DataStoreHelpBox.vue"; -import { type AvailableProject, HelpTextField } from "~/components/data-stores/create/index"; +import { + type AvailableProject, + HelpTextField, +} from "~/components/data-stores/create/index"; import { useToast } from "primevue/usetoast"; -import { type BodyKongInitializeKongInitializePost, DataStoreType, type ProjectNode } from "~/services/Api"; +import { + type BodyKongInitializeKongInitializePost, + DataStoreType, + type ProjectNode, +} from "~/services/Api"; import { getProjectNodes } from "~/composables/useAPIFetch"; +import { + generateRandomDataStoreName, + isValidDataStoreName, +} from "~/utils/data-store-name"; const loading = ref(false); const helpActive = ref(); @@ -86,11 +97,15 @@ const acceptedProtocols = [ "wss", ]; -watch(selectedProject, (newSelectedProject) => { - if (newSelectedProject) { - dataStoreName.value = `${newSelectedProject.id}`; +function rerollDataStoreName() { + if (selectedProject.value) { + dataStoreName.value = generateRandomDataStoreName( + selectedProject.value.name ?? selectedProject.value.id, + ); } -}); +} + +watch(selectedProject, rerollDataStoreName); // Preselect the project when navigated here with a projectId query parameter const route = useRoute(); @@ -142,6 +157,17 @@ function verifyValuesFilled(settings: object): boolean { } async function onSubmitCreateDataStoreAndProject() { + if (!isValidDataStoreName(dataStoreName.value)) { + toast.add({ + severity: "error", + summary: "Invalid data store name", + detail: + "Only letters, digits, '.', '_', '~', and '-' are allowed, and the name cannot be a bare UUID", + life: 5000, + }); + return; + } + const validatedPath = validatePath(path.value); const datastoreSettings = { name: dataStoreName.value, @@ -157,9 +183,9 @@ async function onSubmitCreateDataStoreAndProject() { ds_type: selectedDataStoreType.value, }; - const minioSettings = { - minio_access_key: bucketAccessKey.value || "", - minio_secret_key: bucketSecretKey.value || "", + const s3Settings = { + s3_access_key: bucketAccessKey.value || "", + s3_secret_key: bucketSecretKey.value || "", }; let settingsValidated = verifyValuesFilled(datastoreSettings); @@ -168,8 +194,8 @@ async function onSubmitCreateDataStoreAndProject() { selectedDataStoreType.value === DataStoreType.S3 && selectedBucketAccessPolicy.value === "Private" ) { - configSettings.minio_config = minioSettings; - settingsValidated = settingsValidated && verifyValuesFilled(minioSettings); + configSettings.s3_config = s3Settings; + settingsValidated = settingsValidated && verifyValuesFilled(s3Settings); } if (settingsValidated) { @@ -284,13 +310,27 @@ async function onSubmitCreateDataStoreAndProject() {

Data Store

- + +