diff --git a/client/src/api/configTemplates.ts b/client/src/api/configTemplates.ts index 248ea2b4b934..ec987c7eb3a7 100644 --- a/client/src/api/configTemplates.ts +++ b/client/src/api/configTemplates.ts @@ -19,6 +19,11 @@ export type SecretData = CreateInstancePayload["secrets"]; export type PluginAspectStatus = components["schemas"]["PluginAspectStatus"]; export type PluginStatus = components["schemas"]["PluginStatus"]; +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; diff --git a/client/src/api/objectStores.ts b/client/src/api/objectStores.ts index 55b9a30d39a2..376d75d8e54a 100644 --- a/client/src/api/objectStores.ts +++ b/client/src/api/objectStores.ts @@ -23,8 +23,8 @@ export async function getObjectStoreDetails(id: string) { if (id.startsWith("user_objects://")) { const userObjectStoreId = id.substring("user_objects://".length); - const { data, error } = await GalaxyApi().GET("/api/object_store_instances/{user_object_store_id}", { - params: { path: { user_object_store_id: userObjectStoreId } }, + const { data, error } = await GalaxyApi().GET("/api/object_store_instances/{uuid}", { + params: { path: { uuid: userObjectStoreId } }, }); if (error) { diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index be56f1ba7b3e..a8ddacee9b22 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -906,7 +906,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/file_source_instances/{user_file_source_id}": { + "/api/file_source_instances/{uuid}": { parameters: { query?: never; header?: never; @@ -925,6 +925,24 @@ export interface paths { patch?: never; trace?: never; }; + "/api/file_source_instances/{uuid}/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Test a file source instance and return status. */ + get: operations["file_sources__instances_test_instance"]; + put?: never; + /** Test updating or upgrading user file source instance. */ + post: operations["file_sources__test_instances_update"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/file_source_templates": { parameters: { query?: never; @@ -942,6 +960,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/file_source_templates/{template_id}/{template_version}/oauth2": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Template Oauth2 */ + get: operations["file_sources__template_oauth2"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/folders/{folder_id}/contents": { parameters: { query?: never; @@ -3344,7 +3379,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/object_store_instances/{user_object_store_id}": { + "/api/object_store_instances/{uuid}": { parameters: { query?: never; header?: never; @@ -3363,6 +3398,24 @@ export interface paths { patch?: never; trace?: never; }; + "/api/object_store_instances/{uuid}/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a persisted user object store instance. */ + get: operations["object_stores__instances_test_instance"]; + put?: never; + /** Test updating or upgrading user object source instance. */ + post: operations["object_stores__test_instances_update"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/object_store_templates": { parameters: { query?: never; @@ -5647,6 +5700,23 @@ export interface paths { patch?: never; trace?: never; }; + "/oauth2_callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Callback entry point for remote resource responses with OAuth2 authorization codes */ + get: operations["oauth2_callback_oauth2_callback_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -6818,6 +6888,8 @@ export interface components { template_id: string; /** Template Version */ template_version: number; + /** Uuid */ + uuid?: string | null; /** Variables */ variables: { [key: string]: string | boolean | number; @@ -9035,7 +9107,7 @@ export interface components { * Type * @enum {string} */ - type: "ftp" | "posix" | "s3fs" | "azure" | "onedata" | "webdav"; + type: "ftp" | "posix" | "s3fs" | "azure" | "onedata" | "webdav" | "dropbox" | "googledrive"; /** Variables */ variables?: | ( @@ -14442,6 +14514,11 @@ export interface components { */ updated_count: number; }; + /** OAuth2Info */ + OAuth2Info: { + /** Authorize Url */ + authorize_url: string; + }; /** ObjectExportTaskResponse */ ObjectExportTaskResponse: { /** @@ -14981,6 +15058,7 @@ export interface components { /** PluginStatus */ PluginStatus: { connection?: components["schemas"]["PluginAspectStatus"] | null; + oauth2_access_token_generation?: components["schemas"]["PluginAspectStatus"] | null; template_definition: components["schemas"]["PluginAspectStatus"]; template_settings?: components["schemas"]["PluginAspectStatus"] | null; }; @@ -16515,6 +16593,26 @@ export interface components { */ type: "string"; }; + /** TestUpdateInstancePayload */ + TestUpdateInstancePayload: { + /** Variables */ + variables?: { + [key: string]: string | boolean | number; + } | null; + }; + /** TestUpgradeInstancePayload */ + TestUpgradeInstancePayload: { + /** Secrets */ + secrets: { + [key: string]: string; + }; + /** Template Version */ + template_version: number; + /** Variables */ + variables: { + [key: string]: string | boolean | number; + }; + }; /** ToolDataDetails */ ToolDataDetails: { /** @@ -17366,7 +17464,7 @@ export interface components { * Type * @enum {string} */ - type: "ftp" | "posix" | "s3fs" | "azure" | "onedata" | "webdav"; + type: "ftp" | "posix" | "s3fs" | "azure" | "onedata" | "webdav" | "dropbox" | "googledrive"; /** Uri Root */ uri_root: string; /** @@ -20744,7 +20842,7 @@ export interface operations { }; path: { /** @description The UUID index for a persisted UserFileSourceStore object. */ - user_file_source_id: string; + uuid: string; }; cookie?: never; }; @@ -20788,7 +20886,7 @@ export interface operations { }; path: { /** @description The UUID index for a persisted UserFileSourceStore object. */ - user_file_source_id: string; + uuid: string; }; cookie?: never; }; @@ -20839,7 +20937,7 @@ export interface operations { }; path: { /** @description The UUID index for a persisted UserFileSourceStore object. */ - user_file_source_id: string; + uuid: string; }; cookie?: never; }; @@ -20872,6 +20970,100 @@ export interface operations { }; }; }; + file_sources__instances_test_instance: { + parameters: { + query?: never; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path: { + /** @description The UUID index for a persisted UserFileSourceStore object. */ + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PluginStatus"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; + file_sources__test_instances_update: { + parameters: { + query?: never; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path: { + /** @description The UUID index for a persisted UserFileSourceStore object. */ + uuid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": + | components["schemas"]["TestUpgradeInstancePayload"] + | components["schemas"]["TestUpdateInstancePayload"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PluginStatus"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; file_sources__templates_index: { parameters: { query?: never; @@ -20913,6 +21105,52 @@ export interface operations { }; }; }; + file_sources__template_oauth2: { + parameters: { + query?: never; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path: { + /** @description The template ID of the target file source template. */ + template_id: string; + /** @description The template version of the target file source template. */ + template_version: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OAuth2 authorization url to redirect user to prior to creation. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OAuth2Info"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; index_api_folders__folder_id__contents_get: { parameters: { query?: { @@ -29314,7 +29552,7 @@ export interface operations { }; path: { /** @description The UUID used to identify a persisted UserObjectStore object. */ - user_object_store_id: string; + uuid: string; }; cookie?: never; }; @@ -29358,7 +29596,7 @@ export interface operations { }; path: { /** @description The UUID used to identify a persisted UserObjectStore object. */ - user_object_store_id: string; + uuid: string; }; cookie?: never; }; @@ -29409,7 +29647,7 @@ export interface operations { }; path: { /** @description The UUID used to identify a persisted UserObjectStore object. */ - user_object_store_id: string; + uuid: string; }; cookie?: never; }; @@ -29442,6 +29680,100 @@ export interface operations { }; }; }; + object_stores__instances_test_instance: { + parameters: { + query?: never; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path: { + /** @description The UUID used to identify a persisted UserObjectStore object. */ + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PluginStatus"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; + object_stores__test_instances_update: { + parameters: { + query?: never; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path: { + /** @description The UUID used to identify a persisted UserObjectStore object. */ + uuid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": + | components["schemas"]["TestUpgradeInstancePayload"] + | components["schemas"]["TestUpdateInstancePayload"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PluginStatus"]; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; object_stores__templates_index: { parameters: { query?: never; @@ -36459,4 +36791,50 @@ export interface operations { }; }; }; + oauth2_callback_oauth2_callback_get: { + parameters: { + query: { + /** @description Base-64 encoded JSON used to route request within Galaxy. */ + state: string; + code?: string | null; + error?: string | null; + }; + header?: { + /** @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. */ + "run-as"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Request Error */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + /** @description Server Error */ + "5XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageExceptionModel"]; + }; + }; + }; + }; } diff --git a/client/src/components/ConfigTemplates/ActionSummary.vue b/client/src/components/ConfigTemplates/ActionSummary.vue new file mode 100644 index 000000000000..6e4652a584fe --- /dev/null +++ b/client/src/components/ConfigTemplates/ActionSummary.vue @@ -0,0 +1,29 @@ + + + diff --git a/client/src/components/ConfigTemplates/ConfigurationTestItem.vue b/client/src/components/ConfigTemplates/ConfigurationTestItem.vue new file mode 100644 index 000000000000..91cea8421f0e --- /dev/null +++ b/client/src/components/ConfigTemplates/ConfigurationTestItem.vue @@ -0,0 +1,26 @@ + + + diff --git a/client/src/components/ConfigTemplates/ConfigurationTestSummary.vue b/client/src/components/ConfigTemplates/ConfigurationTestSummary.vue new file mode 100644 index 000000000000..b781c6eae665 --- /dev/null +++ b/client/src/components/ConfigTemplates/ConfigurationTestSummary.vue @@ -0,0 +1,24 @@ + + + diff --git a/client/src/components/ConfigTemplates/ConfigurationTestSummaryModal.vue b/client/src/components/ConfigTemplates/ConfigurationTestSummaryModal.vue new file mode 100644 index 000000000000..22fa8a8b4736 --- /dev/null +++ b/client/src/components/ConfigTemplates/ConfigurationTestSummaryModal.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/src/components/ConfigTemplates/ForceActionButton.vue b/client/src/components/ConfigTemplates/ForceActionButton.vue new file mode 100644 index 000000000000..55d42274f388 --- /dev/null +++ b/client/src/components/ConfigTemplates/ForceActionButton.vue @@ -0,0 +1,17 @@ + + + diff --git a/client/src/components/ConfigTemplates/InstanceDropdown.test.ts b/client/src/components/ConfigTemplates/InstanceDropdown.test.ts index e29d8d883113..abfca1e3070a 100644 --- a/client/src/components/ConfigTemplates/InstanceDropdown.test.ts +++ b/client/src/components/ConfigTemplates/InstanceDropdown.test.ts @@ -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 () => { @@ -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); }); }); diff --git a/client/src/components/ConfigTemplates/InstanceDropdown.vue b/client/src/components/ConfigTemplates/InstanceDropdown.vue index 95bbbf863cb8..0f891a64033e 100644 --- a/client/src/components/ConfigTemplates/InstanceDropdown.vue +++ b/client/src/components/ConfigTemplates/InstanceDropdown.vue @@ -1,6 +1,5 @@ @@ -32,7 +30,7 @@ const emit = defineEmits<{ aria-haspopup="true" aria-expanded="false" class="ui-link font-weight-bold text-nowrap"> - + {{ name }} diff --git a/client/src/components/ConfigTemplates/InstanceForm.vue b/client/src/components/ConfigTemplates/InstanceForm.vue index 129870128228..530cb0b95f75 100644 --- a/client/src/components/ConfigTemplates/InstanceForm.vue +++ b/client/src/components/ConfigTemplates/InstanceForm.vue @@ -3,6 +3,7 @@ import { BButton } from "bootstrap-vue"; import { type FormEntry } from "./formUtil"; +import ForceActionButton from "./ForceActionButton.vue"; import FormCard from "@/components/Form/FormCard.vue"; import FormDisplay from "@/components/Form/FormDisplay.vue"; import LoadingSpan from "@/components/LoadingSpan.vue"; @@ -12,14 +13,18 @@ interface Props { inputs?: Array; // not fully reactive so make sure to not mutate this array submitTitle: string; loadingMessage: string; + busy: boolean; + showForceActionButton?: boolean; } withDefaults(defineProps(), { inputs: undefined, + showForceActionButton: false, }); const emit = defineEmits<{ (e: "onSubmit", formData: any): void; + (e: "onForceSubmit", formData: any): void; }>(); let formData: any; @@ -31,6 +36,10 @@ function onChange(incoming: any) { async function handleSubmit() { emit("onSubmit", formData); } + +async function handleForceSubmit() { + emit("onForceSubmit", formData); +}
- + {{ submitTitle }} + +
diff --git a/client/src/components/ConfigTemplates/routing.ts b/client/src/components/ConfigTemplates/routing.ts new file mode 100644 index 000000000000..d8b63d60ca8d --- /dev/null +++ b/client/src/components/ConfigTemplates/routing.ts @@ -0,0 +1,18 @@ +import { useRouter } from "vue-router/composables"; + +export function buildInstanceRoutingComposable(index: string) { + return () => { + const router = useRouter(); + + async function goToIndex(query: Record<"message", string>) { + router.push({ + path: index, + query: query, + }); + } + + return { + goToIndex, + }; + }; +} diff --git a/client/src/components/ConfigTemplates/test_fixtures.ts b/client/src/components/ConfigTemplates/test_fixtures.ts index d3a8f867c0c4..8e5b967318c1 100644 --- a/client/src/components/ConfigTemplates/test_fixtures.ts +++ b/client/src/components/ConfigTemplates/test_fixtures.ts @@ -1,3 +1,4 @@ +import { type PluginStatus } from "@/api/configTemplates"; import { type FileSourceTemplateSummary } from "@/api/fileSources"; import { type UserConcreteObjectStore } from "@/components/ObjectStore/Instances/types"; import { type ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types"; @@ -134,3 +135,18 @@ export const OBJECT_STORE_INSTANCE: UserConcreteObjectStore = { hidden: false, purged: false, }; + +export const OK_PLUGIN_STATUS: PluginStatus = { + template_definition: { + state: "ok", + message: "ok", + }, + template_settings: { + state: "ok", + message: "ok", + }, + connection: { + state: "ok", + message: "ok", + }, +}; diff --git a/client/src/components/ConfigTemplates/useConfigurationTesting.ts b/client/src/components/ConfigTemplates/useConfigurationTesting.ts new file mode 100644 index 000000000000..26ec1389cf8e --- /dev/null +++ b/client/src/components/ConfigTemplates/useConfigurationTesting.ts @@ -0,0 +1,387 @@ +import { computed, type Ref, ref } from "vue"; + +import { GalaxyApi } from "@/api"; +import { type Instance, type PluginStatus, type TemplateSummary } from "@/api/configTemplates"; +import { type buildInstanceRoutingComposable } from "@/components/ConfigTemplates/routing"; +import { errorMessageAsString } from "@/utils/simple-error"; + +import { + createFormDataToPayload, + createTemplateForm, + editFormDataToPayload, + editTemplateForm, + type FormEntry, + pluginStatusToErrorMessage, + upgradeForm, + upgradeFormDataToPayload, +} from "./formUtil"; + +import ActionSummary from "./ActionSummary.vue"; +import ConfigurationTestSummaryModal from "@/components/ConfigTemplates/ConfigurationTestSummaryModal.vue"; +import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue"; + +type InstanceRoutingComposableType = ReturnType; + +export function useConfigurationTesting() { + const testRunning = ref(false); + const testResults = ref(undefined); + + return { + testRunning, + testResults, + }; +} + +type CreateTestUrl = "/api/object_store_instances/test" | "/api/file_source_instances/test"; +type CreateUrl = "/api/object_store_instances" | "/api/file_source_instances"; + +export function useConfigurationTemplateCreation( + what: string, + template: Ref, + uuid: Ref, + testUrl: CreateTestUrl, + createUrl: CreateUrl, + onCreate: (result: R) => unknown +) { + const error = ref(null); + const { testRunning, testResults } = useConfigurationTesting(); + + async function onSubmit(formData: any) { + const payload = createFormDataToPayload(template.value, formData); + if (uuid.value) { + payload.uuid = uuid.value; + } + let pluginStatus: PluginStatus; + try { + testRunning.value = true; + const { data, error: testRequestError } = await GalaxyApi().POST(testUrl, { + body: payload, + }); + if (testRequestError) { + error.value = "Failed to verify configuration: " + errorMessageAsString(testRequestError); + return; + } else { + pluginStatus = data; + testResults.value = pluginStatus; + } + } catch (e) { + error.value = "Failed to verify configuration: " + errorMessageAsString(e); + return; + } finally { + testRunning.value = false; + } + if (pluginStatus) { + const testError = pluginStatusToErrorMessage(pluginStatus); + if (testError) { + error.value = testError; + return; + } + } + try { + const { data: userObject, error: createRequestError } = await GalaxyApi().POST(createUrl, { + body: payload, + }); + if (createRequestError) { + error.value = errorMessageAsString(createRequestError); + } else { + onCreate(userObject as R); + } + } catch (e) { + error.value = errorMessageAsString(e); + return; + } + } + + const inputs = computed(() => { + return createTemplateForm(template.value, what); + }); + + const submitTitle = "Create"; + const loadingMessage = `Loading ${what} template and instance information`; + + return { + ActionSummary, + error, + inputs, + InstanceForm, + onSubmit, + submitTitle, + loadingMessage, + testRunning, + testResults, + }; +} + +type TestInstanceUrl = "/api/file_source_instances/{uuid}/test" | "/api/object_store_instances/{uuid}/test"; + +export function useInstanceTesting(testUrl: TestInstanceUrl) { + const showTestResults = ref(false); + const testResults = ref(undefined); + const testingError = ref(undefined); + + async function test(instance: R) { + testResults.value = undefined; + testingError.value = undefined; + showTestResults.value = true; + try { + const { data, error } = await GalaxyApi().GET(testUrl, { + params: { path: { uuid: instance.uuid } }, + }); + const pluginStatus = data; + const testRequestError = error; + if (testRequestError) { + testingError.value = errorMessageAsString(testRequestError); + } else { + testResults.value = pluginStatus; + } + } catch (e) { + testingError.value = errorMessageAsString(e); + } + } + + return { + ConfigurationTestSummaryModal, + showTestResults, + testResults, + testingError, + test, + }; +} + +type TestUpdateUrl = "/api/object_store_instances/{uuid}/test" | "/api/file_source_instances/{uuid}/test"; +type UpdateUrl = "/api/object_store_instances/{uuid}" | "/api/file_source_instances/{uuid}"; + +export function useConfigurationTemplateEdit( + what: string, + instance: Ref, + template: Ref, + testUpdateUrl: TestUpdateUrl, + updateUrl: UpdateUrl, + useRouting: InstanceRoutingComposableType +) { + const { testRunning, testResults } = useConfigurationTesting(); + const showForceActionButton = ref(false); + + const { goToIndex } = useRouting(); + + async function onUpdate(objectStore: R) { + const message = `Updated ${what} ${objectStore.name}`; + goToIndex({ message }); + } + + const inputs = computed | undefined>(() => { + const realizedInstance = instance.value; + const realizedTemplate = template.value; + if (!realizedInstance || !realizedTemplate) { + return undefined; + } + return editTemplateForm(realizedTemplate, what, realizedInstance); + }); + + const error = ref(null); + const hasSecrets = computed(() => instance.value?.secrets && instance.value?.secrets.length > 0); + const submitTitle = "Update Settings"; + const loadingMessage = `Loading ${what} template and instance information`; + + async function onSubmit(formData: any) { + if (template.value && instance.value) { + const payload = editFormDataToPayload(template.value, formData); + + let pluginStatus; + try { + testRunning.value = true; + showForceActionButton.value = false; + const { data: pluginStatus, error: testRequestError } = await GalaxyApi().POST(testUpdateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (testRequestError) { + error.value = errorMessageAsString(testRequestError); + showForceActionButton.value = true; + } + testResults.value = pluginStatus; + } catch (e) { + error.value = errorMessageAsString(e); + showForceActionButton.value = true; + return; + } finally { + testRunning.value = false; + } + if (pluginStatus) { + const testError = pluginStatusToErrorMessage(pluginStatus); + if (testError) { + error.value = testError; + showForceActionButton.value = true; + return; + } + } + + try { + const { data, error: updateRequestError } = await GalaxyApi().PUT(updateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (updateRequestError) { + error.value = errorMessageAsString(updateRequestError); + } else { + await onUpdate(data as R); + } + } catch (e) { + error.value = errorMessageAsString(e); + return; + } + } + } + + async function onForceSubmit(formData: any) { + if (template.value && instance.value) { + const payload = editFormDataToPayload(template.value, formData); + try { + const { data, error: updateRequestError } = await GalaxyApi().PUT(updateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (updateRequestError) { + error.value = errorMessageAsString(updateRequestError); + } else { + await onUpdate(data as R); + } + } catch (e) { + error.value = errorMessageAsString(e); + return; + } + } + } + + return { + error, + ActionSummary, + inputs, + InstanceForm, + hasSecrets, + loadingMessage, + onForceSubmit, + onSubmit, + showForceActionButton, + submitTitle, + testResults, + testRunning, + }; +} + +export function useConfigurationTemplateUpgrade( + what: string, + instance: Ref, + template: Ref, + testUpdateUrl: TestUpdateUrl, + updateUrl: UpdateUrl, + useRouting: InstanceRoutingComposableType +) { + const { goToIndex } = useRouting(); + + async function onUpgrade(objectStore: R) { + const message = `Upgraded ${what} ${objectStore.name}`; + goToIndex({ message }); + } + + const error = ref(null); + const { testRunning, testResults } = useConfigurationTesting(); + const showForceActionButton = ref(false); + + const submitTitle = "Upgrade Configuration"; + const loadingMessage = `Loading latest ${what} template and instance information`; + + const inputs = computed | undefined>(() => { + const realizedInstance = instance.value; + const realizedLatestTemplate = template.value; + if (!realizedInstance || !realizedLatestTemplate) { + return undefined; + } + const form = upgradeForm(realizedLatestTemplate, realizedInstance); + return form; + }); + + async function onSubmit(formData: any) { + if (!instance.value || !template.value) { + return; + } + + const payload = upgradeFormDataToPayload(template.value, formData); + let pluginStatus; + try { + testRunning.value = true; + showForceActionButton.value = false; + const { data: pluginStatus, error: testRequestError } = await GalaxyApi().POST(testUpdateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (testRequestError) { + error.value = errorMessageAsString(testRequestError); + showForceActionButton.value = true; + return; + } else { + testResults.value = pluginStatus; + } + } catch (e) { + showForceActionButton.value = true; + error.value = errorMessageAsString(e); + return; + } finally { + testRunning.value = false; + } + if (pluginStatus) { + const testError = pluginStatusToErrorMessage(pluginStatus); + if (testError) { + error.value = testError; + showForceActionButton.value = true; + return; + } + } + try { + const { data, error: updateRequestError } = await GalaxyApi().PUT(updateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (updateRequestError) { + error.value = errorMessageAsString(updateRequestError); + } else { + await onUpgrade(data as R); + } + } catch (e) { + error.value = errorMessageAsString(e); + return; + } + } + + async function onForceSubmit(formData: any) { + const payload = upgradeFormDataToPayload(template.value, formData); + try { + const { data, error: updateRequestError } = await GalaxyApi().PUT(updateUrl, { + params: { path: { uuid: instance.value.uuid } }, + body: payload, + }); + if (updateRequestError) { + error.value = errorMessageAsString(updateRequestError); + } else { + await onUpgrade(data as R); + } + } catch (e) { + error.value = errorMessageAsString(e); + return; + } + } + + return { + error, + ActionSummary, + inputs, + InstanceForm, + loadingMessage, + onForceSubmit, + onSubmit, + submitTitle, + showForceActionButton, + testResults, + testRunning, + }; +} diff --git a/client/src/components/FileSources/FileSourceTypeSpan.vue b/client/src/components/FileSources/FileSourceTypeSpan.vue index d98cdce9d622..890649e4cf86 100644 --- a/client/src/components/FileSources/FileSourceTypeSpan.vue +++ b/client/src/components/FileSources/FileSourceTypeSpan.vue @@ -4,12 +4,14 @@ import { computed } from "vue"; import type { FileSourceTypes } from "@/api/fileSources"; const MESSAGES = { - posix: "This is a simple path based storage location that assumes the all the relevant paths are already mounted on the Galaxy server and target worker nodes.", - s3fs: "This is an remote file source plugin based on the Amazon Simple Storage Service (S3) interface. The AWS interface has become an industry standard and many storage vendors support it and use it to expose 'object' based storage.", - azure: "This is an remote file source plugin based on the Azure service.", - onedata: "This is an remote file source plugin based on the Onedata service.", - ftp: "This is an remote file source plugin based on the FTP protocol.", - webdav: "This is an remote file source plugin based on the WebDAV protocol.", + posix: "This is a simple path based file source that assumes the all the relevant paths are already mounted on the Galaxy server and target worker nodes.", + s3fs: "This is a remote file source plugin based on the Amazon Simple Storage Service (S3) interface. The AWS interface has become an industry standard and many storage vendors support it and use it to expose 'object' based storage.", + azure: "This is a remote file source plugin based on the Azure service.", + onedata: "This is a remote file source plugin based on the Onedata service.", + ftp: "This is a remote file source plugin based on the FTP protocol.", + webdav: "This is a remote file source plugin based on the WebDAV protocol.", + dropbox: "This is a file source plugin that connects with the commercial Dropbox service.", + googledrive: "This is a file source plugin that connects with the commercial Google Drive service.", }; interface Props { diff --git a/client/src/components/FileSources/Instances/CreateForm.vue b/client/src/components/FileSources/Instances/CreateForm.vue index c9e712af60e0..0fb8eb433583 100644 --- a/client/src/components/FileSources/Instances/CreateForm.vue +++ b/client/src/components/FileSources/Instances/CreateForm.vue @@ -1,73 +1,42 @@ diff --git a/client/src/components/FileSources/Instances/CreateInstance.vue b/client/src/components/FileSources/Instances/CreateInstance.vue index 9b93e2d7c269..ed6def6c2f86 100644 --- a/client/src/components/FileSources/Instances/CreateInstance.vue +++ b/client/src/components/FileSources/Instances/CreateInstance.vue @@ -1,8 +1,11 @@ diff --git a/client/src/components/FileSources/Instances/EditInstance.vue b/client/src/components/FileSources/Instances/EditInstance.vue index 16069ffe58da..01469088fd16 100644 --- a/client/src/components/FileSources/Instances/EditInstance.vue +++ b/client/src/components/FileSources/Instances/EditInstance.vue @@ -2,16 +2,15 @@ import { BTab, BTabs } from "bootstrap-vue"; import { computed, ref } from "vue"; -import { GalaxyApi } from "@/api"; -import type { UserFileSourceModel } from "@/api/fileSources"; -import { editFormDataToPayload, editTemplateForm, type FormEntry } from "@/components/ConfigTemplates/formUtil"; -import { rethrowSimple } from "@/utils/simple-error"; +import { useConfigurationTemplateEdit } from "@/components/ConfigTemplates/useConfigurationTesting"; import { useInstanceAndTemplate } from "./instance"; import { useInstanceRouting } from "./routing"; import EditSecrets from "./EditSecrets.vue"; -import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue"; + +const editTestUrl = "/api/file_source_instances/{uuid}/test"; +const editUrl = "/api/file_source_instances/{uuid}"; interface Props { instanceId: string; @@ -20,52 +19,40 @@ interface Props { const props = defineProps(); const { instance, template } = useInstanceAndTemplate(ref(props.instanceId)); -const inputs = computed | undefined>(() => { - if (template.value && instance.value) { - return editTemplateForm(template.value, "storage location", instance.value); - } - return undefined; -}); - -const title = computed(() => `Edit File Source ${instance.value?.name} Settings`); -const hasSecrets = computed(() => instance.value?.secrets && instance.value?.secrets.length > 0); -const submitTitle = "Update Settings"; -const loadingMessage = "Loading file source template and instance information"; - -async function onSubmit(formData: any) { - if (template.value && instance.value) { - const payload = editFormDataToPayload(template.value, formData); - const user_file_source_id = instance.value.uuid; - - const { data: fileSource, error } = await GalaxyApi().PUT("/api/file_source_instances/{user_file_source_id}", { - params: { path: { user_file_source_id } }, - body: payload, - }); - - if (error) { - rethrowSimple(error); - } +const title = computed(() => `Edit ${template.value?.name} settings for ${instance.value?.name}`); +const errorDataDescription = "file-source-update-error"; - await onUpdate(fileSource); - } -} - -const { goToIndex } = useInstanceRouting(); - -async function onUpdate(instance: UserFileSourceModel) { - const message = `Updated file source ${instance.name}`; - goToIndex({ message }); -} +const { + error, + hasSecrets, + ActionSummary, + inputs, + InstanceForm, + loadingMessage, + onForceSubmit, + onSubmit, + testRunning, + testResults, + submitTitle, + showForceActionButton, +} = useConfigurationTemplateEdit("file source", instance, template, editTestUrl, editUrl, useInstanceRouting); diff --git a/client/src/components/FileSources/Instances/EditSecrets.vue b/client/src/components/FileSources/Instances/EditSecrets.vue index 6ae4b88b3240..1a3346f401ac 100644 --- a/client/src/components/FileSources/Instances/EditSecrets.vue +++ b/client/src/components/FileSources/Instances/EditSecrets.vue @@ -15,8 +15,8 @@ const props = defineProps(); const title = computed(() => `Update File Source ${props.fileSource?.name} Secrets`); async function onUpdate(secretName: string, secretValue: string) { - const { error } = await GalaxyApi().PUT("/api/file_source_instances/{user_file_source_id}", { - params: { path: { user_file_source_id: props.fileSource.uuid } }, + const { error } = await GalaxyApi().PUT("/api/file_source_instances/{uuid}", { + params: { path: { uuid: props.fileSource.uuid } }, body: { secret_name: secretName, secret_value: secretValue, diff --git a/client/src/components/FileSources/Instances/InstanceDropdown.vue b/client/src/components/FileSources/Instances/InstanceDropdown.vue index 1bd11d78af6d..0dd02f396aa4 100644 --- a/client/src/components/FileSources/Instances/InstanceDropdown.vue +++ b/client/src/components/FileSources/Instances/InstanceDropdown.vue @@ -22,8 +22,8 @@ const isUpgradable = computed(() => ); async function onRemove() { - const { error } = await GalaxyApi().PUT("/api/file_source_instances/{user_file_source_id}", { - params: { path: { user_file_source_id: props.fileSource.uuid } }, + const { error } = await GalaxyApi().PUT("/api/file_source_instances/{uuid}", { + params: { path: { uuid: props.fileSource.uuid } }, body: { hidden: true, }, @@ -38,6 +38,7 @@ async function onRemove() { const emit = defineEmits<{ (e: "entryRemoved"): void; + (e: "test"): void; }>(); @@ -48,5 +49,6 @@ const emit = defineEmits<{ :is-upgradable="isUpgradable" :route-upgrade="routeUpgrade" :route-edit="routeEdit" + @test="emit('test')" @remove="onRemove" /> diff --git a/client/src/components/FileSources/Instances/ManageIndex.vue b/client/src/components/FileSources/Instances/ManageIndex.vue index 784e0b84a685..1ecc522d0aca 100644 --- a/client/src/components/FileSources/Instances/ManageIndex.vue +++ b/client/src/components/FileSources/Instances/ManageIndex.vue @@ -3,6 +3,7 @@ import { BTable } from "bootstrap-vue"; import { computed } from "vue"; import { DESCRIPTION_FIELD, NAME_FIELD, TEMPLATE_FIELD, TYPE_FIELD } from "@/components/ConfigTemplates/fields"; +import { useInstanceTesting } from "@/components/ConfigTemplates/useConfigurationTesting"; import { useFiltering } from "@/components/ConfigTemplates/useInstanceFiltering"; import { useFileSourceInstancesStore } from "@/stores/fileSourceInstancesStore"; @@ -30,10 +31,16 @@ fileSourceInstancesStore.fetchInstances(); function reload() { fileSourceInstancesStore.fetchInstances(); } + +const testInstanceUrl = "/api/file_source_instances/{uuid}/test"; + +const { ConfigurationTestSummaryModal, showTestResults, testResults, test, testingError } = + useInstanceTesting(testInstanceUrl); diff --git a/client/src/components/ObjectStore/Instances/ManageIndex.vue b/client/src/components/ObjectStore/Instances/ManageIndex.vue index c30d1def5210..f4d4f419737e 100644 --- a/client/src/components/ObjectStore/Instances/ManageIndex.vue +++ b/client/src/components/ObjectStore/Instances/ManageIndex.vue @@ -4,11 +4,11 @@ import { computed } from "vue"; import type { UserConcreteObjectStore } from "@/api/objectStores"; import { DESCRIPTION_FIELD, NAME_FIELD, TEMPLATE_FIELD, TYPE_FIELD } from "@/components/ConfigTemplates/fields"; +import { useInstanceTesting } from "@/components/ConfigTemplates/useConfigurationTesting"; import { useFiltering } from "@/components/ConfigTemplates/useInstanceFiltering"; import { useObjectStoreInstancesStore } from "@/stores/objectStoreInstancesStore"; import _l from "@/utils/localization"; -import InstanceDropdown from "./InstanceDropdown.vue"; import ManageIndexHeader from "@/components/ConfigTemplates/ManageIndexHeader.vue"; import LoadingSpan from "@/components/LoadingSpan.vue"; import ObjectStoreBadges from "@/components/ObjectStore/ObjectStoreBadges.vue"; @@ -39,6 +39,11 @@ objectStoreInstancesStore.fetchInstances(); function reload() { objectStoreInstancesStore.fetchInstances(); } + +const testInstanceUrl = "/api/object_store_instances/{uuid}/test"; + +const { ConfigurationTestSummaryModal, showTestResults, testResults, test, testingError } = + useInstanceTesting(testInstanceUrl);