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
-
+
+
@@ -568,6 +608,7 @@ async function onSubmitCreateDataStoreAndProject() {
gap: 0.6rem;
margin-bottom: 1rem;
}
+
.data-store-input-fields {
width: 80%;
margin-right: 24px;
diff --git a/app/components/header/CleanupDialog.vue b/app/components/header/CleanupDialog.vue
index a17af81e..47a2cb0a 100644
--- a/app/components/header/CleanupDialog.vue
+++ b/app/components/header/CleanupDialog.vue
@@ -1,5 +1,5 @@