Skip to content

Commit

Permalink
Expand pre-checking of file source/object store configuration.
Browse files Browse the repository at this point in the history
- UI for detailed display of errors.
- UI option to test configuration from management menu.
- API + UI for checking configuration before upgrading to new version of template.
- API + UI for checking configuration before updating current template's settings.
- Add an option during update/upgrade to allow forcing the update even if configuration doesn't validate - I don't allow creation of invalid things, but if there are problems with an existing thing - admins and power users should have recourse. It is their data.
  • Loading branch information
jmchilton committed Sep 26, 2024
1 parent 5e332f0 commit e7dbd0a
Show file tree
Hide file tree
Showing 37 changed files with 1,436 additions and 391 deletions.
6 changes: 6 additions & 0 deletions client/src/api/configTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export type SecretData = CreateInstancePayload["secrets"];
export type PluginAspectStatus = components["schemas"]["PluginAspectStatus"];
export type PluginStatus = components["schemas"]["PluginStatus"];

export type CreateInstancePayload = components["schemas"]["CreateInstancePayload"];
export type UpgradeInstancePayload = components["schemas"]["UpgradeInstancePayload"];
export type TestUpgradeInstancePayload = components["schemas"]["TestUpgradeInstancePayload"];
export type UpdateInstancePayload = components["schemas"]["UpdateInstancePayload"];
export type TestUpdateInstancePayload = components["schemas"]["TestUpdateInstancePayload"];

export interface TemplateSummary {
description: string | null;
hidden?: boolean;
Expand Down
154 changes: 154 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,12 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/file_source_instances/{user_file_source_id}/test": {
/** Test a file source instance and return status. */
get: operations["file_sources__instances_test_instance"];
/** Test updating or upgrading user file source instance. */
post: operations["file_sources__test_instances_update"];
};
"/api/file_source_templates": {
parameters: {
query?: never;
Expand Down Expand Up @@ -3306,6 +3312,12 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/object_store_instances/{user_object_store_id}/test": {
/** Get a persisted user object store instance. */
get: operations["object_stores__instances_test_instance"];
/** Test updating or upgrading user object source instance. */
post: operations["object_stores__test_instances_update"];
};
"/api/object_store_templates": {
parameters: {
query?: never;
Expand Down Expand Up @@ -15775,6 +15787,26 @@ export interface components {
*/
type: "string";
};
/** TestUpdateInstancePayload */
TestUpdateInstancePayload: {
/** Variables */
variables?: {
[key: string]: (string | boolean | number) | undefined;
} | null;
};
/** TestUpgradeInstancePayload */
TestUpgradeInstancePayload: {
/** Secrets */
secrets: {
[key: string]: string | undefined;
};
/** Template Version */
template_version: number;
/** Variables */
variables: {
[key: string]: (string | boolean | number) | undefined;
};
};
/** ToolDataDetails */
ToolDataDetails: {
/**
Expand Down Expand Up @@ -20100,6 +20132,67 @@ export interface operations {
};
};
};
file_sources__instances_test_instance: {
/** Test a file source instance and return status. */
parameters: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
/** @description The UUID index for a persisted UserFileSourceStore object. */
path: {
user_file_source_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
file_sources__test_instances_update: {
/** Test updating or upgrading user file source instance. */
parameters: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
/** @description The UUID index for a persisted UserFileSourceStore object. */
path: {
user_file_source_id: string;
};
};
requestBody: {
content: {
"application/json":
| components["schemas"]["TestUpgradeInstancePayload"]
| components["schemas"]["TestUpdateInstancePayload"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
file_sources__templates_index: {
parameters: {
query?: never;
Expand Down Expand Up @@ -28431,6 +28524,67 @@ export interface operations {
};
};
};
object_stores__instances_test_instance: {
/** Get a persisted user object store instance. */
parameters: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
/** @description The UUID used to identify a persisted UserObjectStore object. */
path: {
user_object_store_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
object_stores__test_instances_update: {
/** Test updating or upgrading user object source instance. */
parameters: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
/** @description The UUID used to identify a persisted UserObjectStore object. */
path: {
user_object_store_id: string;
};
};
requestBody: {
content: {
"application/json":
| components["schemas"]["TestUpgradeInstancePayload"]
| components["schemas"]["TestUpdateInstancePayload"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["PluginStatus"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
object_stores__templates_index: {
parameters: {
query?: never;
Expand Down
29 changes: 29 additions & 0 deletions client/src/components/ConfigTemplates/ActionSummary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { BAlert, BButton } from "bootstrap-vue";
import { ref } from "vue";
import type { PluginStatus } from "@/api/configTemplates";
import ConfigurationTestSummaryModal from "@/components/ConfigTemplates/ConfigurationTestSummaryModal.vue";
interface Props {
error: String | null;
testResults?: PluginStatus;
errorDataDescription: string;
}
const showTestResults = ref(false);
defineProps<Props>();
</script>

<template>
<div>
<ConfigurationTestSummaryModal v-model="showTestResults" :test-results="testResults" />
<BAlert v-if="error" variant="danger" class="configuration-instance-error" show>
<span :data-description="errorDataDescription">
{{ error }}
</span>
<BButton variant="link" @click="showTestResults = true">View configuration test status.</BButton>
</BAlert>
</div>
</template>
32 changes: 32 additions & 0 deletions client/src/components/ConfigTemplates/ConfigurationTestItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCheckSquare, faSquare, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BListGroupItem, BSpinner } from "bootstrap-vue";
import type { PluginAspectStatus } from "@/api/configTemplates";
library.add(faCheckSquare, faTimes, faSquare);
interface Props {
status?: PluginAspectStatus;
}
defineProps<Props>();
</script>

<template>
<BListGroupItem href="#" class="d-flex align-items-center">
<BSpinner v-if="status == undefined" class="mr-3" label="Testing...."></BSpinner>
<FontAwesomeIcon
v-else-if="status.state == 'ok'"
class="mr-3 text-success"
icon="fas fa-check-square"
size="lg" />
<FontAwesomeIcon v-else-if="status.state == 'not_ok'" class="mr-3 text-warning" icon="fas fa-times" size="lg" />
<FontAwesomeIcon v-else-if="status.state == 'unknown'" class="mr-3 text-info" icon="fas fa-square" size="lg" />
<span v-if="status && status.message" class="mr-auto">
{{ status.message }}
</span>
</BListGroupItem>
</template>
21 changes: 21 additions & 0 deletions client/src/components/ConfigTemplates/ConfigurationTestSummary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import { BListGroup } from "bootstrap-vue";
import type { PluginStatus } from "@/api/configTemplates";
import ConfigurationTestItem from "./ConfigurationTestItem.vue";
interface Props {
testResults?: PluginStatus;
}
defineProps<Props>();
</script>

<template>
<BListGroup v-if="testResults">
<ConfigurationTestItem :status="testResults?.template_definition" />
<ConfigurationTestItem :status="testResults?.template_settings" />
<ConfigurationTestItem :status="testResults?.connection" />
</BListGroup>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { BAlert, BModal } from "bootstrap-vue";
import { ref, watch } from "vue";
import type { PluginStatus } from "@/api/configTemplates";
import ConfigurationTestSummary from "./ConfigurationTestSummary.vue";
interface Props {
value: boolean;
testResults?: PluginStatus;
error?: string;
}
const props = defineProps<Props>();
const show = ref(props.value);
watch(props, () => {
show.value = props.value;
});
const emit = defineEmits<{
(e: "input", value: boolean): void;
}>();
watch(show, () => {
emit("input", show.value);
});
</script>

<template>
<BModal v-model="show" title="Configuration Test Summary" hide-footer>
<BAlert v-if="error" variant="danger" show dismissible>
{{ error || "" }}
</BAlert>
<ConfigurationTestSummary :test-results="testResults" />
</BModal>
</template>
17 changes: 17 additions & 0 deletions client/src/components/ConfigTemplates/ForceActionButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { BButton } from "bootstrap-vue";
interface Props {
action: string;
}
defineProps<Props>();
const emit = defineEmits<{
(e: "click"): void;
}>();
</script>

<template>
<BButton variant="link" @click="emit('click')">I know what I am doing, force {{ action.toLowerCase() }}.</BButton>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("InstanceDropdown", () => {
});
const menu = wrapper.find(".dropdown-menu");
const links = menu.findAll("button.dropdown-item");
expect(links.length).toBe(2);
expect(links.length).toBe(3);
});

it("should render a drop down with upgrade if upgrade available as an option", async () => {
Expand All @@ -35,6 +35,6 @@ describe("InstanceDropdown", () => {
});
const menu = wrapper.find(".dropdown-menu");
const links = menu.findAll("button.dropdown-item");
expect(links.length).toBe(3);
expect(links.length).toBe(4);
});
});
9 changes: 7 additions & 2 deletions client/src/components/ConfigTemplates/InstanceDropdown.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faArrowUp, faCaretDown, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faArrowUp, faCaretDown, faEdit, faStethoscope, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useRouter } from "vue-router/composables";
Expand All @@ -12,14 +12,15 @@ interface Props {
isUpgradable: boolean;
}
library.add(faArrowUp, faCaretDown, faEdit, faTrash);
library.add(faArrowUp, faCaretDown, faEdit, faStethoscope, faTrash);
const router = useRouter();
defineProps<Props>();
const emit = defineEmits<{
(e: "remove"): void;
(e: "test"): void;
}>();
</script>

Expand Down Expand Up @@ -53,6 +54,10 @@ const emit = defineEmits<{
<FontAwesomeIcon icon="edit" />
<span v-localize>Edit configuration</span>
</button>
<button class="dropdown-item" @keypress="emit('test')" @click.prevent="emit('test')">
<FontAwesomeIcon icon="stethoscope" />
<span v-localize>Test instance</span>
</button>
<button class="dropdown-item" @keypress="emit('remove')" @click.prevent="emit('remove')">
<FontAwesomeIcon icon="trash" />
<span v-localize>Remove instance</span>
Expand Down
Loading

0 comments on commit e7dbd0a

Please sign in to comment.