Implement file source templates
@@ -0,0 +1,25 @@
|
||||
import type { components } from "@/api/schema/schema";
|
||||
|
||||
export type Instance =
|
||||
| components["schemas"]["UserFileSourceModel"]
|
||||
| components["schemas"]["UserConcreteObjectStoreModel"];
|
||||
|
||||
export type TemplateVariable =
|
||||
| components["schemas"]["TemplateVariableString"]
|
||||
| components["schemas"]["TemplateVariableInteger"]
|
||||
| components["schemas"]["TemplateVariablePathComponent"]
|
||||
| components["schemas"]["TemplateVariableBoolean"];
|
||||
export type TemplateSecret = components["schemas"]["TemplateSecret"];
|
||||
export type VariableValueType = (string | boolean | number) | undefined;
|
||||
export type VariableData = { [key: string]: VariableValueType };
|
||||
export type SecretData = { [key: string]: string };
|
||||
|
||||
export interface TemplateSummary {
|
||||
description: string | null;
|
||||
hidden?: boolean;
|
||||
id: string;
|
||||
name: string | null;
|
||||
secrets?: TemplateSecret[] | null;
|
||||
variables?: TemplateVariable[] | null;
|
||||
version?: number;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type components } from "@/api/schema";
|
||||
|
||||
export type FileSourceTemplateSummary = components["schemas"]["FileSourceTemplateSummary"];
|
||||
export type FileSourceTemplateSummaries = FileSourceTemplateSummary[];
|
||||
|
||||
export type UserFileSourceModel = components["schemas"]["UserFileSourceModel"];
|
||||
@@ -1,4 +1,9 @@
|
||||
import { fetcher } from "@/api/schema";
|
||||
import type { components } from "@/api/schema/schema";
|
||||
|
||||
export type UserConcreteObjectStore = components["schemas"]["UserConcreteObjectStoreModel"];
|
||||
|
||||
export type ObjectStoreTemplateType = "aws_s3" | "azure_blob" | "boto3" | "disk" | "generic_s3";
|
||||
|
||||
const getObjectStores = fetcher.path("/api/object_stores").method("get").create();
|
||||
|
||||
|
||||
@@ -318,6 +318,24 @@ export interface paths {
|
||||
/** Download */
|
||||
get: operations["download_api_drs_download__object_id__get"];
|
||||
};
|
||||
"/api/file_source_instances": {
|
||||
/** Get a list of persisted file source instances defined by the requesting user. */
|
||||
get: operations["file_sources__instances_index"];
|
||||
/** Create a user-bound object store. */
|
||||
post: operations["file_sources__create_instance"];
|
||||
};
|
||||
"/api/file_source_instances/{user_file_source_id}": {
|
||||
/** Get a list of persisted file source instances defined by the requesting user. */
|
||||
get: operations["file_sources__instances_get"];
|
||||
/** Update or upgrade user file source instance. */
|
||||
put: operations["file_sources__instances_update"];
|
||||
/** Purge user file source instance. */
|
||||
delete: operations["file_sources__instances_purge"];
|
||||
};
|
||||
"/api/file_source_templates": {
|
||||
/** Get a list of file source templates available to build user defined file sources from */
|
||||
get: operations["file_sources__templates_index"];
|
||||
};
|
||||
"/api/folders/{folder_id}/contents": {
|
||||
/**
|
||||
* Returns a list of a folder's contents (files and sub-folders) with additional metadata about the folder.
|
||||
@@ -1256,6 +1274,8 @@ export interface paths {
|
||||
get: operations["object_stores__instances_get"];
|
||||
/** Update or upgrade user object store instance. */
|
||||
put: operations["object_stores__instances_update"];
|
||||
/** Purge user object store instance. */
|
||||
delete: operations["object_stores__instances_purge"];
|
||||
};
|
||||
"/api/object_store_templates": {
|
||||
/** Get a list of object store templates available to build user defined object stores from */
|
||||
@@ -5238,6 +5258,43 @@ export interface components {
|
||||
*/
|
||||
update_time: string;
|
||||
};
|
||||
/** FileSourceTemplateSummaries */
|
||||
FileSourceTemplateSummaries: components["schemas"]["FileSourceTemplateSummary"][];
|
||||
/** FileSourceTemplateSummary */
|
||||
FileSourceTemplateSummary: {
|
||||
/** Description */
|
||||
description: string | null;
|
||||
/**
|
||||
* Hidden
|
||||
* @default false
|
||||
*/
|
||||
hidden?: boolean;
|
||||
/** Id */
|
||||
id: string;
|
||||
/** Name */
|
||||
name: string | null;
|
||||
/** Secrets */
|
||||
secrets?: components["schemas"]["TemplateSecret"][] | null;
|
||||
/**
|
||||
* Type
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "ftp" | "posix" | "s3fs" | "azure";
|
||||
/** Variables */
|
||||
variables?:
|
||||
| (
|
||||
| components["schemas"]["TemplateVariableString"]
|
||||
| components["schemas"]["TemplateVariableInteger"]
|
||||
| components["schemas"]["TemplateVariablePathComponent"]
|
||||
| components["schemas"]["TemplateVariableBoolean"]
|
||||
)[]
|
||||
| null;
|
||||
/**
|
||||
* Version
|
||||
* @default 0
|
||||
*/
|
||||
version?: number;
|
||||
};
|
||||
/** FilesSourcePlugin */
|
||||
FilesSourcePlugin: {
|
||||
/**
|
||||
@@ -9929,13 +9986,6 @@ export interface components {
|
||||
*/
|
||||
up_to_date: boolean;
|
||||
};
|
||||
/** ObjectStoreTemplateSecret */
|
||||
ObjectStoreTemplateSecret: {
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/** ObjectStoreTemplateSummaries */
|
||||
ObjectStoreTemplateSummaries: components["schemas"]["ObjectStoreTemplateSummary"][];
|
||||
/** ObjectStoreTemplateSummary */
|
||||
@@ -9954,32 +10004,27 @@ export interface components {
|
||||
/** Name */
|
||||
name: string | null;
|
||||
/** Secrets */
|
||||
secrets?: components["schemas"]["ObjectStoreTemplateSecret"][] | null;
|
||||
secrets?: components["schemas"]["TemplateSecret"][] | null;
|
||||
/**
|
||||
* Type
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "s3" | "azure_blob" | "disk" | "generic_s3";
|
||||
type: "aws_s3" | "azure_blob" | "boto3" | "disk" | "generic_s3";
|
||||
/** Variables */
|
||||
variables?: components["schemas"]["ObjectStoreTemplateVariable"][] | null;
|
||||
variables?:
|
||||
| (
|
||||
| components["schemas"]["TemplateVariableString"]
|
||||
| components["schemas"]["TemplateVariableInteger"]
|
||||
| components["schemas"]["TemplateVariablePathComponent"]
|
||||
| components["schemas"]["TemplateVariableBoolean"]
|
||||
)[]
|
||||
| null;
|
||||
/**
|
||||
* Version
|
||||
* @default 0
|
||||
*/
|
||||
version?: number;
|
||||
};
|
||||
/** ObjectStoreTemplateVariable */
|
||||
ObjectStoreTemplateVariable: {
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "string" | "boolean" | "integer";
|
||||
};
|
||||
/** OutputReferenceByLabel */
|
||||
OutputReferenceByLabel: {
|
||||
/**
|
||||
@@ -11856,6 +11901,92 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
TaskState: "PENDING" | "STARTED" | "RETRY" | "FAILURE" | "SUCCESS";
|
||||
/** TemplateSecret */
|
||||
TemplateSecret: {
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Label */
|
||||
label?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
};
|
||||
/** TemplateVariableBoolean */
|
||||
TemplateVariableBoolean: {
|
||||
/**
|
||||
* Default
|
||||
* @default false
|
||||
*/
|
||||
default?: boolean;
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Label */
|
||||
label?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "boolean";
|
||||
};
|
||||
/** TemplateVariableInteger */
|
||||
TemplateVariableInteger: {
|
||||
/**
|
||||
* Default
|
||||
* @default 0
|
||||
*/
|
||||
default?: number;
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Label */
|
||||
label?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "integer";
|
||||
};
|
||||
/** TemplateVariablePathComponent */
|
||||
TemplateVariablePathComponent: {
|
||||
/** Default */
|
||||
default?: string | null;
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Label */
|
||||
label?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "path_component";
|
||||
};
|
||||
/** TemplateVariableString */
|
||||
TemplateVariableString: {
|
||||
/**
|
||||
* Default
|
||||
* @default
|
||||
*/
|
||||
default?: string;
|
||||
/** Help */
|
||||
help: string | null;
|
||||
/** Label */
|
||||
label?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "string";
|
||||
};
|
||||
/** ToolDataDetails */
|
||||
ToolDataDetails: {
|
||||
/**
|
||||
@@ -12200,8 +12331,12 @@ export interface components {
|
||||
};
|
||||
/** UpdateInstancePayload */
|
||||
UpdateInstancePayload: {
|
||||
/** Active */
|
||||
active?: boolean | null;
|
||||
/** Description */
|
||||
description?: string | null;
|
||||
/** Hidden */
|
||||
hidden?: boolean | null;
|
||||
/** Name */
|
||||
name?: string | null;
|
||||
/** Variables */
|
||||
@@ -12513,12 +12648,16 @@ export interface components {
|
||||
};
|
||||
/** UserConcreteObjectStoreModel */
|
||||
UserConcreteObjectStoreModel: {
|
||||
/** Active */
|
||||
active: boolean;
|
||||
/** Badges */
|
||||
badges: components["schemas"]["BadgeDict"][];
|
||||
/** Description */
|
||||
description?: string | null;
|
||||
/** Device */
|
||||
device?: string | null;
|
||||
/** Hidden */
|
||||
hidden: boolean;
|
||||
/** Id */
|
||||
id: number | string;
|
||||
/** Name */
|
||||
@@ -12527,6 +12666,8 @@ export interface components {
|
||||
object_store_id?: string | null;
|
||||
/** Private */
|
||||
private: boolean;
|
||||
/** Purged */
|
||||
purged: boolean;
|
||||
quota: components["schemas"]["QuotaModel"];
|
||||
/** Secrets */
|
||||
secrets: string[];
|
||||
@@ -12538,7 +12679,7 @@ export interface components {
|
||||
* Type
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "s3" | "azure_blob" | "disk" | "generic_s3";
|
||||
type: "aws_s3" | "azure_blob" | "boto3" | "disk" | "generic_s3";
|
||||
/** Uuid */
|
||||
uuid: string;
|
||||
/** Variables */
|
||||
@@ -12588,6 +12729,40 @@ export interface components {
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
/** UserFileSourceModel */
|
||||
UserFileSourceModel: {
|
||||
/** Active */
|
||||
active: boolean;
|
||||
/** Description */
|
||||
description: string | null;
|
||||
/** Hidden */
|
||||
hidden: boolean;
|
||||
/** Id */
|
||||
id: string | number;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Purged */
|
||||
purged: boolean;
|
||||
/** Secrets */
|
||||
secrets: string[];
|
||||
/** Template Id */
|
||||
template_id: string;
|
||||
/** Template Version */
|
||||
template_version: number;
|
||||
/**
|
||||
* Type
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "ftp" | "posix" | "s3fs" | "azure";
|
||||
/** Uri Root */
|
||||
uri_root: string;
|
||||
/** Uuid */
|
||||
uuid: string;
|
||||
/** Variables */
|
||||
variables: {
|
||||
[key: string]: (string | boolean | number) | undefined;
|
||||
} | null;
|
||||
};
|
||||
/**
|
||||
* UserModel
|
||||
* @description User in a transaction context.
|
||||
@@ -14777,6 +14952,169 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__instances_index: {
|
||||
/** Get a list of persisted file source instances defined by the requesting user. */
|
||||
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;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserFileSourceModel"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__create_instance: {
|
||||
/** Create a user-bound object store. */
|
||||
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;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateInstancePayload"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserFileSourceModel"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__instances_get: {
|
||||
/** Get a list of persisted file source instances defined by the requesting user. */
|
||||
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 index for a persisted UserFileSourceStore object. */
|
||||
path: {
|
||||
user_file_source_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserFileSourceModel"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__instances_update: {
|
||||
/** Update or upgrade 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 index for a persisted UserFileSourceStore object. */
|
||||
path: {
|
||||
user_file_source_id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["UpdateInstanceSecretPayload"]
|
||||
| components["schemas"]["UpgradeInstancePayload"]
|
||||
| components["schemas"]["UpdateInstancePayload"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserFileSourceModel"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__instances_purge: {
|
||||
/** Purge 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 index for a persisted UserFileSourceStore object. */
|
||||
path: {
|
||||
user_file_source_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
file_sources__templates_index: {
|
||||
/** Get a list of file source templates available to build user defined file sources from */
|
||||
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;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description A list of the configured file source templates. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["FileSourceTemplateSummaries"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
index_api_folders__folder_id__contents_get: {
|
||||
/**
|
||||
* Returns a list of a folder's contents (files and sub-folders) with additional metadata about the folder.
|
||||
@@ -20608,6 +20946,33 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
object_stores__instances_purge: {
|
||||
/** Purge 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 identifier used to index a persisted UserObjectStore object. */
|
||||
path: {
|
||||
user_object_store_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
object_stores__templates_index: {
|
||||
/** Get a list of object store templates available to build user defined object stores from */
|
||||
parameters?: {
|
||||
@@ -20647,7 +21012,10 @@ export interface operations {
|
||||
/** @description A list of the configured object stores. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ConcreteObjectStoreModel"][];
|
||||
"application/json": (
|
||||
| components["schemas"]["ConcreteObjectStoreModel"]
|
||||
| components["schemas"]["UserConcreteObjectStoreModel"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import CreateInstance from "./CreateInstance.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("CreateInstance", () => {
|
||||
it("should render a loading message during loading", async () => {
|
||||
const wrapper = shallowMount(CreateInstance, {
|
||||
propsData: {
|
||||
loading: true,
|
||||
loadingMessage: "component loading...",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const loadingSpan = wrapper.findComponent({ name: "LoadingSpan" }).exists();
|
||||
expect(loadingSpan).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide a loading message after loading", async () => {
|
||||
const wrapper = shallowMount(CreateInstance, {
|
||||
propsData: {
|
||||
loading: false,
|
||||
loadingMessage: "component loading...",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const loadingSpan = wrapper.findComponent({ name: "LoadingSpan" }).exists();
|
||||
expect(loadingSpan).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { BContainer } from "bootstrap-vue";
|
||||
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
loadingMessage: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BContainer fluid class="p-0">
|
||||
<LoadingSpan v-if="loading" :message="loadingMessage" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { STANDARD_FILE_SOURCE_TEMPLATE, STANDARD_OBJECT_STORE_TEMPLATE } from "./test_fixtures";
|
||||
|
||||
import EditSecretsForm from "./EditSecretsForm.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("EditSecretsForm", () => {
|
||||
it("should render a secrets for for file source templates", async () => {
|
||||
const wrapper = mount(EditSecretsForm, {
|
||||
propsData: {
|
||||
template: STANDARD_FILE_SOURCE_TEMPLATE,
|
||||
title: "Secrets FORM for file source",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const titleText = wrapper.find(".portlet-title-text");
|
||||
expect(titleText.exists()).toBeTruthy();
|
||||
expect(titleText.text()).toEqual("Secrets FORM for file source");
|
||||
});
|
||||
|
||||
it("should render a secrets for for object store templates", async () => {
|
||||
const wrapper = mount(EditSecretsForm, {
|
||||
propsData: {
|
||||
template: STANDARD_OBJECT_STORE_TEMPLATE,
|
||||
title: "Secrets FORM for object store",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const titleText = wrapper.find(".portlet-title-text");
|
||||
expect(titleText.exists()).toBeTruthy();
|
||||
expect(titleText.text()).toEqual("Secrets FORM for object store");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { TemplateSummary } from "@/api/configTemplates";
|
||||
|
||||
import VaultSecret from "./VaultSecret.vue";
|
||||
import FormCard from "@/components/Form/FormCard.vue";
|
||||
|
||||
interface Props {
|
||||
template: TemplateSummary;
|
||||
title: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update", secretName: string, secretValue: string): void;
|
||||
}>();
|
||||
|
||||
async function update(secretName: string, secretValue: string) {
|
||||
emit("update", secretName, secretValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormCard :title="title">
|
||||
<template v-slot:body>
|
||||
<div>
|
||||
<div v-for="secret in template.secrets" :key="secret.name">
|
||||
<VaultSecret
|
||||
:label="secret.label || secret.name"
|
||||
:name="secret.name"
|
||||
:help="secret.help || ''"
|
||||
:is-set="true"
|
||||
@update="update">
|
||||
</VaultSecret>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormCard>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import InstanceDropdown from "./InstanceDropdown.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("InstanceDropdown", () => {
|
||||
it("should render a drop down without upgrade if upgrade unavailable as an option", async () => {
|
||||
const wrapper = shallowMount(InstanceDropdown, {
|
||||
propsData: {
|
||||
prefix: "file-source",
|
||||
name: "my cool instance",
|
||||
routeEdit: "/object_store_instance/edit",
|
||||
routeUpgrade: "/object_store_instance/upgrade",
|
||||
isUpgradable: false,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const menu = wrapper.find(".dropdown-menu");
|
||||
const links = menu.findAll("a");
|
||||
expect(links.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should render a drop down with upgrade if upgrade available as an option", async () => {
|
||||
const wrapper = shallowMount(InstanceDropdown, {
|
||||
propsData: {
|
||||
prefix: "file-source",
|
||||
name: "my cool instance",
|
||||
routeEdit: "/object_store_instance/edit",
|
||||
routeUpgrade: "/object_store_instance/upgrade",
|
||||
isUpgradable: true,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const menu = wrapper.find(".dropdown-menu");
|
||||
const links = menu.findAll("a");
|
||||
expect(links.length).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import "./icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BLink } from "bootstrap-vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
interface Props {
|
||||
prefix: string;
|
||||
name: string;
|
||||
routeEdit: string;
|
||||
routeUpgrade: string;
|
||||
isUpgradable: boolean;
|
||||
}
|
||||
|
||||
const title = "";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "remove"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BLink
|
||||
v-b-tooltip.hover
|
||||
:class="`${prefix}-instance-dropdown font-weight-bold`"
|
||||
data-toggle="dropdown"
|
||||
:title="title"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<FontAwesomeIcon icon="caret-down" />
|
||||
<span class="instance-dropdown-name">{{ name }}</span>
|
||||
</BLink>
|
||||
<div class="dropdown-menu" :aria-labelledby="`${prefix}-instance-dropdown`">
|
||||
<a
|
||||
v-if="isUpgradable"
|
||||
class="dropdown-item"
|
||||
@keypress="router.push(routeUpgrade)"
|
||||
@click.prevent="router.push(routeUpgrade)">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Upgrade</span>
|
||||
</a>
|
||||
<a class="dropdown-item" @keypress="router.push(routeEdit)" @click.prevent="router.push(routeEdit)">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Edit configuration</span>
|
||||
</a>
|
||||
<a class="dropdown-item" @keypress="emit('remove')" @click.prevent="emit('remove')">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Remove instance</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import InstanceForm from "./InstanceForm.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
const inputs: any[] = [];
|
||||
const SUBMIT_TITLE = "Submit the form!";
|
||||
|
||||
describe("InstanceForm", () => {
|
||||
it("should render a loading message and not submit button if inputs is null", async () => {
|
||||
const wrapper = shallowMount(InstanceForm, {
|
||||
propsData: {
|
||||
title: "MY FORM",
|
||||
loading: true,
|
||||
inputs: null,
|
||||
submitTitle: SUBMIT_TITLE,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const loadingSpan = wrapper.findComponent({ name: "LoadingSpan" }).exists();
|
||||
expect(loadingSpan).toBeTruthy();
|
||||
expect(wrapper.find("#submit").exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should hide a loading message after loading", async () => {
|
||||
const wrapper = shallowMount(InstanceForm, {
|
||||
propsData: {
|
||||
title: "MY FORM",
|
||||
loading: false,
|
||||
inputs: inputs,
|
||||
submitTitle: SUBMIT_TITLE,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const loadingSpan = wrapper.findComponent({ name: "LoadingSpan" }).exists();
|
||||
expect(loadingSpan).toBeFalsy();
|
||||
expect(wrapper.find("#submit").exists()).toBeTruthy();
|
||||
expect(wrapper.find("#submit").text()).toEqual(SUBMIT_TITLE);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { BButton } from "bootstrap-vue";
|
||||
|
||||
import FormCard from "@/components/Form/FormCard.vue";
|
||||
import FormDisplay from "@/components/Form/FormDisplay.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
@@ -7,6 +9,7 @@ interface Props {
|
||||
title: string;
|
||||
inputs: any | null; // not fully reactive so make sure these are ready to go when loading is false
|
||||
submitTitle: string;
|
||||
loadingMessage: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
@@ -26,7 +29,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<LoadingSpan v-if="inputs == null" message="Loading storage location template and instance information" />
|
||||
<LoadingSpan v-if="inputs == null" :message="loadingMessage" />
|
||||
<div v-else>
|
||||
<FormCard :title="title">
|
||||
<template v-slot:body>
|
||||
@@ -34,9 +37,9 @@ async function handleSubmit() {
|
||||
</template>
|
||||
</FormCard>
|
||||
<div class="mt-3">
|
||||
<b-button id="submit" variant="primary" class="mr-1" @click="handleSubmit">
|
||||
<BButton id="submit" variant="primary" class="mr-1" @click="handleSubmit">
|
||||
{{ submitTitle }}
|
||||
</b-button>
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import "./icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert, BButton, BCol, BRow } from "bootstrap-vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import _l from "@/utils/localization";
|
||||
|
||||
interface Props {
|
||||
message: String | null | undefined;
|
||||
createButtonId: string;
|
||||
createRoute: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BAlert v-if="message" show dismissible>
|
||||
{{ message || "" }}
|
||||
</BAlert>
|
||||
<BRow class="mb-3">
|
||||
<BCol>
|
||||
<BButton :id="createButtonId" class="m-1 float-right" @click="router.push(createRoute)">
|
||||
<FontAwesomeIcon icon="plus" />
|
||||
{{ _l("Create") }}
|
||||
</BButton>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { STANDARD_FILE_SOURCE_TEMPLATE } from "./test_fixtures";
|
||||
|
||||
import SelectTemplate from "./SelectTemplate.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
const help = "some help text about selection";
|
||||
|
||||
describe("SelectTemplate", () => {
|
||||
it("should render a selection row for supplied templates", async () => {
|
||||
const wrapper = mount(SelectTemplate, {
|
||||
propsData: {
|
||||
templates: [STANDARD_FILE_SOURCE_TEMPLATE],
|
||||
selectText: help,
|
||||
idPrefix: "file-source",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
console.log(wrapper.html());
|
||||
const helpText = wrapper.find(".file-source-template-select-help");
|
||||
expect(helpText.exists()).toBeTruthy();
|
||||
expect(helpText.text()).toBeLocalizationOf(help);
|
||||
const buttons = wrapper.findAll("button");
|
||||
expect(buttons.length).toBe(1);
|
||||
const button = buttons.at(0);
|
||||
expect(button.attributes().id).toEqual("file-source-template-button-moo");
|
||||
expect(button.attributes()["data-template-id"]).toEqual("moo");
|
||||
expect(button.text()).toEqual("moo");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import { BButton, BButtonGroup, BCol, BRow } from "bootstrap-vue";
|
||||
|
||||
import type { TemplateSummary } from "@/api/configTemplates";
|
||||
|
||||
interface Props {
|
||||
selectText: string;
|
||||
idPrefix: string;
|
||||
templates: TemplateSummary[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onSubmit", id: string): void;
|
||||
}>();
|
||||
|
||||
async function handleSubmit(templateId: string) {
|
||||
emit("onSubmit", templateId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BRow>
|
||||
<BCol cols="7">
|
||||
<BButtonGroup vertical size="lg" style="width: 100%">
|
||||
<BButton
|
||||
v-for="template in templates"
|
||||
:id="`${idPrefix}-template-button-${template.id}`"
|
||||
:key="template.id"
|
||||
:class="`${idPrefix}-template-select-button`"
|
||||
:data-template-id="template.id"
|
||||
@click="handleSubmit(template.id)"
|
||||
>{{ template.name }}
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BCol>
|
||||
<BCol cols="5">
|
||||
<p v-localize style="float: right" :class="`${idPrefix}-template-select-help`">
|
||||
{{ selectText }}
|
||||
</p>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { STANDARD_FILE_SOURCE_TEMPLATE } from "./test_fixtures";
|
||||
|
||||
import TemplateSummaryPopover from "./TemplateSummaryPopover.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("TemplateSummaryPopover", () => {
|
||||
it("should render a secrets for for file source templates", async () => {
|
||||
const wrapper = shallowMount(TemplateSummaryPopover, {
|
||||
propsData: {
|
||||
template: STANDARD_FILE_SOURCE_TEMPLATE,
|
||||
target: "popover-target",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const popover = wrapper.findComponent({ name: "BPopover" });
|
||||
expect(popover.attributes().target).toEqual("popover-target");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { BPopover } from "bootstrap-vue";
|
||||
|
||||
import type { TemplateSummary } from "@/api/configTemplates";
|
||||
|
||||
interface Props {
|
||||
target: String;
|
||||
template: TemplateSummary;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const popoverPlacement = "rightbottom";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BPopover :target="target" triggers="hover" boundary="window" :placement="popoverPlacement">
|
||||
<template v-slot:title>{{ props.template.name }}</template>
|
||||
<slot />
|
||||
</BPopover>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import VaultSecret from "./VaultSecret.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("VaultSecret", () => {
|
||||
it("should render a form element", async () => {
|
||||
const wrapper = shallowMount(VaultSecret, {
|
||||
propsData: {
|
||||
name: "secret name",
|
||||
label: "Label Secret",
|
||||
help: "here is some good *help*",
|
||||
isSet: true,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const titleWrapper = wrapper.find(".ui-form-title-text");
|
||||
expect(titleWrapper.text()).toEqual("Label Secret");
|
||||
const helpWrapper = wrapper.find(".ui-form-info p");
|
||||
// verify markdown converted
|
||||
expect(helpWrapper.html()).toEqual("<p>here is some good <em>help</em></p>");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { BButton, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { markup } from "@/components/ObjectStore/configurationMarkdown";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label: string;
|
||||
help: string;
|
||||
isSet: boolean;
|
||||
}
|
||||
@@ -10,7 +14,8 @@ const props = defineProps<Props>();
|
||||
|
||||
const showEdit = ref<boolean>(false);
|
||||
const secretValue = ref<string>("");
|
||||
const editTitle = computed(() => `Edit ${props.name}`);
|
||||
const editTitle = computed(() => `Edit ${props.label}`);
|
||||
const helpHtml = computed(() => markup(props.help, true));
|
||||
|
||||
function onClick() {
|
||||
showEdit.value = true;
|
||||
@@ -29,37 +34,35 @@ async function onOk() {
|
||||
<div class="ui-form-element section-row">
|
||||
<div class="ui-form-title">
|
||||
<div class="ui-form-title-text">
|
||||
{{ name }}
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-form-field">
|
||||
<div>
|
||||
<b-input-group>
|
||||
<b-form-input type="password" value="*****************************" disabled @click="onClick" />
|
||||
<b-input-group-append>
|
||||
<b-button @click="onClick">
|
||||
<BInputGroup>
|
||||
<BFormInput type="password" value="*****************************" disabled @click="onClick" />
|
||||
<BInputGroupAppend>
|
||||
<BButton @click="onClick">
|
||||
<icon icon="edit" />
|
||||
Update
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</BButton>
|
||||
</BInputGroupAppend>
|
||||
</BInputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ui-form-info form-text text-muted">
|
||||
{{ help }}
|
||||
</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="ui-form-info form-text text-muted" v-html="helpHtml" />
|
||||
</div>
|
||||
<b-modal ref="edit-modal" v-model="showEdit" :title="editTitle" ok-title="Update" @ok="onOk">
|
||||
<div>
|
||||
<b-form-input v-model="secretValue" type="password" />
|
||||
<span class="ui-form-info form-text text-muted">
|
||||
{{ help }}
|
||||
</span>
|
||||
<BFormInput v-model="secretValue" type="password" />
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="ui-form-info form-text text-muted" v-html="helpHtml" />
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../Form/form-elements.scss";
|
||||
@import "../Form/form-elements.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
import _l from "@/utils/localization";
|
||||
|
||||
export const NAME_FIELD = {
|
||||
key: "name",
|
||||
label: _l("Name"),
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
export const DESCRIPTION_FIELD = {
|
||||
key: "description",
|
||||
label: _l("Description"),
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
export const TYPE_FIELD = {
|
||||
key: "type",
|
||||
label: _l("Type"),
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
export const TEMPLATE_FIELD = {
|
||||
key: "template",
|
||||
label: _l("From Template"),
|
||||
sortable: true,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { TemplateVariable } from "@/api/configTemplates";
|
||||
|
||||
import { createTemplateForm, templateVariableFormEntry, upgradeForm } from "./formUtil";
|
||||
import {
|
||||
GENERIC_FTP_FILE_SOURCE_TEMPLATE,
|
||||
OBJECT_STORE_INSTANCE,
|
||||
STANDARD_FILE_SOURCE_TEMPLATE,
|
||||
STANDARD_OBJECT_STORE_TEMPLATE,
|
||||
} from "./test_fixtures";
|
||||
|
||||
const FTP_VARIABLES = GENERIC_FTP_FILE_SOURCE_TEMPLATE.variables as TemplateVariable[];
|
||||
const PROJECT_VARIABLE = {
|
||||
name: "Project",
|
||||
type: "path_component",
|
||||
} as TemplateVariable;
|
||||
|
||||
describe("formUtils", () => {
|
||||
describe("createTemplateForm", () => {
|
||||
it("should create a form from an object store templates", () => {
|
||||
const form = createTemplateForm(STANDARD_OBJECT_STORE_TEMPLATE, "storage location");
|
||||
expect(form.length).toBe(6);
|
||||
const formEl0 = form[0];
|
||||
expect(formEl0?.name).toEqual("_meta_name");
|
||||
expect(formEl0?.help).toEqual("Label this new storage location with a name.");
|
||||
const formEl1 = form[1];
|
||||
expect(formEl1?.name).toEqual("_meta_description");
|
||||
});
|
||||
|
||||
it("should create a form from a file source templates", () => {
|
||||
const form = createTemplateForm(STANDARD_FILE_SOURCE_TEMPLATE, "file source");
|
||||
expect(form.length).toBe(6);
|
||||
const formEl0 = form[0];
|
||||
expect(formEl0?.name).toEqual("_meta_name");
|
||||
expect(formEl0?.help).toEqual("Label this new file source with a name.");
|
||||
const formEl1 = form[1];
|
||||
expect(formEl1?.name).toEqual("_meta_description");
|
||||
});
|
||||
});
|
||||
|
||||
describe("upgradeForm", () => {
|
||||
it("should create a form from an object store templates", () => {
|
||||
const form = upgradeForm(STANDARD_OBJECT_STORE_TEMPLATE, OBJECT_STORE_INSTANCE);
|
||||
expect(form.length).toBe(3);
|
||||
const formEl0 = form[0];
|
||||
expect(formEl0?.name).toEqual("oldvar");
|
||||
const formEl1 = form[1];
|
||||
expect(formEl1?.name).toEqual("newvar");
|
||||
});
|
||||
|
||||
it("should only ask for new secrets during upgrade", () => {
|
||||
const form = upgradeForm(STANDARD_OBJECT_STORE_TEMPLATE, OBJECT_STORE_INSTANCE);
|
||||
expect(form.length).toBe(3);
|
||||
const formEl0 = form[2];
|
||||
expect(formEl0?.name).toEqual("newsecret");
|
||||
expect(formEl0?.type).toEqual("password");
|
||||
});
|
||||
});
|
||||
|
||||
describe("templateVariableFormEntry", () => {
|
||||
it("should render string types as Galaxy text form inputs", () => {
|
||||
const hostVariable = FTP_VARIABLES[0] as TemplateVariable;
|
||||
const formEntry = templateVariableFormEntry(hostVariable, undefined);
|
||||
expect(formEntry.name).toBe("host");
|
||||
expect(formEntry.label).toBe("FTP Host");
|
||||
expect(formEntry.type).toBe("text");
|
||||
expect(formEntry.help).toBe("<p>Host of FTP Server to connect to.</p>\n");
|
||||
});
|
||||
it("should render integer types as Galaxy integer inputs", () => {
|
||||
const portVariable = FTP_VARIABLES[3] as TemplateVariable;
|
||||
const formEntry = templateVariableFormEntry(portVariable, undefined);
|
||||
expect(formEntry.name).toBe("port");
|
||||
expect(formEntry.label).toBe("FTP Port");
|
||||
expect(formEntry.type).toBe("integer");
|
||||
expect(formEntry.value).toBe(21);
|
||||
});
|
||||
it("should render boolean types as Galaxy boolean inputs", () => {
|
||||
const writableVariable = FTP_VARIABLES[2] as TemplateVariable;
|
||||
const formEntry = templateVariableFormEntry(writableVariable, undefined);
|
||||
expect(formEntry.name).toBe("writable");
|
||||
expect(formEntry.label).toBe("Writable?");
|
||||
expect(formEntry.type).toBe("boolean");
|
||||
expect(formEntry.value).toBe(false);
|
||||
});
|
||||
it("should render path_component types as Galaxy text form inputs", () => {
|
||||
const formEntry = templateVariableFormEntry(PROJECT_VARIABLE, undefined);
|
||||
expect(formEntry.name).toBe("Project");
|
||||
expect(formEntry.label).toBe("Project");
|
||||
expect(formEntry.type).toBe("text");
|
||||
});
|
||||
it("should render path_component updated default values if supplied", () => {
|
||||
const formEntry = templateVariableFormEntry(PROJECT_VARIABLE, "foobar");
|
||||
expect(formEntry.value).toBe("foobar");
|
||||
});
|
||||
it("should render string types with updated default values if supplied", () => {
|
||||
const hostVariable = FTP_VARIABLES[0] as TemplateVariable;
|
||||
const formEntry = templateVariableFormEntry(hostVariable, "mycoolhost.org");
|
||||
expect(formEntry.value).toBe("mycoolhost.org");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
import type {
|
||||
Instance,
|
||||
SecretData,
|
||||
TemplateSecret,
|
||||
TemplateSummary,
|
||||
TemplateVariable,
|
||||
VariableData,
|
||||
VariableValueType,
|
||||
} from "@/api/configTemplates";
|
||||
import { markup } from "@/components/ObjectStore/configurationMarkdown";
|
||||
|
||||
export function metadataFormEntryName(what: string) {
|
||||
return {
|
||||
name: "_meta_name",
|
||||
label: "Name",
|
||||
type: "text",
|
||||
optional: false,
|
||||
help: `Label this new ${what} with a name.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function metadataFormEntryDescription(what: string) {
|
||||
return {
|
||||
name: "_meta_description",
|
||||
label: "Description",
|
||||
optional: true,
|
||||
type: "textarea",
|
||||
help: `Provide some notes to yourself about this ${what} - perhaps to remind you how it is configured, where it stores the data, etc..`,
|
||||
};
|
||||
}
|
||||
|
||||
export function templateVariableFormEntry(variable: TemplateVariable, variableValue: VariableValueType) {
|
||||
const common_fields = {
|
||||
name: variable.name,
|
||||
label: variable.label ?? variable.name,
|
||||
help: markup(variable.help || "", true),
|
||||
};
|
||||
if (variable.type == "string") {
|
||||
const defaultValue = variable.default ?? "";
|
||||
return {
|
||||
type: "text",
|
||||
value: variableValue == undefined ? defaultValue : variableValue,
|
||||
...common_fields,
|
||||
};
|
||||
} else if (variable.type == "path_component") {
|
||||
const defaultValue = variable.default ?? "";
|
||||
// TODO: do extra validation with form somehow...
|
||||
return {
|
||||
type: "text",
|
||||
value: variableValue == undefined ? defaultValue : variableValue,
|
||||
...common_fields,
|
||||
};
|
||||
} else if (variable.type == "integer") {
|
||||
const defaultValue = variable.default ?? 0;
|
||||
return {
|
||||
type: "integer",
|
||||
value: variableValue == undefined ? defaultValue : variableValue,
|
||||
...common_fields,
|
||||
};
|
||||
} else if (variable.type == "boolean") {
|
||||
const defaultValue = variable.default ?? false;
|
||||
return {
|
||||
type: "boolean",
|
||||
value: variableValue == undefined ? defaultValue : variableValue,
|
||||
...common_fields,
|
||||
};
|
||||
} else {
|
||||
throw Error("Invalid template form input type found.");
|
||||
}
|
||||
}
|
||||
|
||||
export function templateSecretFormEntry(secret: TemplateSecret) {
|
||||
return {
|
||||
name: secret.name,
|
||||
label: secret.label ?? secret.name,
|
||||
type: "password",
|
||||
help: markup(secret.help || "", true),
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function editTemplateForm(template: TemplateSummary, what: string, instance: Instance) {
|
||||
const form = [];
|
||||
const nameInput = metadataFormEntryName(what);
|
||||
form.push({ value: instance.name ?? "", ...nameInput });
|
||||
|
||||
const descriptionInput = metadataFormEntryDescription(what);
|
||||
form.push({ value: instance.description ?? "", ...descriptionInput });
|
||||
|
||||
const variables = template.variables ?? [];
|
||||
const variableValues: VariableData = instance.variables || {};
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, variableValues[variable.name]));
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
export function editFormDataToPayload(template: TemplateSummary, formData: any) {
|
||||
const variables = template.variables ?? [];
|
||||
const name = formData["_meta_name"];
|
||||
const description = formData["_meta_description"];
|
||||
const variableData: VariableData = {};
|
||||
for (const variable of variables) {
|
||||
const variableValue = formDataTypedGet(variable, formData);
|
||||
if (variableValue !== undefined) {
|
||||
variableData[variable.name] = variableValue;
|
||||
}
|
||||
}
|
||||
const payload = {
|
||||
name: name,
|
||||
description: description,
|
||||
variables: variableData,
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createTemplateForm(template: TemplateSummary, what: string) {
|
||||
const form = [];
|
||||
const variables = template.variables ?? [];
|
||||
const secrets = template.secrets ?? [];
|
||||
form.push(metadataFormEntryName(what));
|
||||
form.push(metadataFormEntryDescription(what));
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, undefined));
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
form.push(templateSecretFormEntry(secret));
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
export function createFormDataToPayload(template: TemplateSummary, formData: any) {
|
||||
const variables = template.variables ?? [];
|
||||
const secrets = template.secrets ?? [];
|
||||
const variableData: VariableData = {};
|
||||
const secretData: SecretData = {};
|
||||
for (const variable of variables) {
|
||||
const variableValue = formDataTypedGet(variable, formData);
|
||||
if (variableValue !== undefined) {
|
||||
variableData[variable.name] = variableValue;
|
||||
}
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
secretData[secret.name] = formData[secret.name];
|
||||
}
|
||||
const name: string = formData._meta_name;
|
||||
const description: string = formData._meta_description;
|
||||
const payload = {
|
||||
name: name,
|
||||
description: description,
|
||||
secrets: secretData,
|
||||
variables: variableData,
|
||||
template_id: template.id,
|
||||
template_version: template.version ?? 0,
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function formDataTypedGet(variableDefinition: TemplateVariable, formData: any): VariableValueType {
|
||||
// galaxy form library doesn't type values traditionally, so add a typed
|
||||
// access to the data if coming back as string. Though it does seem to be
|
||||
// typed properly - this might not be needed anymore?
|
||||
const variableType = variableDefinition.type;
|
||||
const variableName = variableDefinition.name;
|
||||
const rawValue: boolean | string | number | null | undefined = formData[variableName];
|
||||
if (variableType == "string") {
|
||||
if (rawValue == null || rawValue == undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return String(rawValue);
|
||||
}
|
||||
} else if (variableType == "path_component") {
|
||||
if (rawValue == null || rawValue == undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return String(rawValue);
|
||||
}
|
||||
} else if (variableType == "boolean") {
|
||||
if (rawValue == null || rawValue == undefined || typeof rawValue == "number") {
|
||||
return undefined;
|
||||
} else {
|
||||
return String(rawValue).toLowerCase() == "true";
|
||||
}
|
||||
} else if (variableType == "integer") {
|
||||
if (rawValue == null || rawValue == undefined || typeof rawValue == "boolean") {
|
||||
return undefined;
|
||||
} else {
|
||||
if (typeof rawValue == "string") {
|
||||
return parseInt(rawValue);
|
||||
} else {
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Error("Unknown variable type encountered, shouldn't be possible.");
|
||||
}
|
||||
}
|
||||
|
||||
export function upgradeForm(template: TemplateSummary, instance: Instance): Array<any> {
|
||||
const form = [];
|
||||
const variables = template.variables ?? [];
|
||||
const secrets = template.secrets ?? [];
|
||||
const variableValues: VariableData = instance.variables || {};
|
||||
const secretsSet = instance.secrets || [];
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, variableValues[variable.name]));
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
const secretName = secret.name;
|
||||
if (secretsSet.indexOf(secretName) >= 0) {
|
||||
console.log("skipping...");
|
||||
} else {
|
||||
form.push(templateSecretFormEntry(secret));
|
||||
}
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
export function upgradeFormDataToPayload(template: TemplateSummary, formData: any) {
|
||||
const variables = template.variables ?? [];
|
||||
const variableData: VariableData = {};
|
||||
for (const variable of variables) {
|
||||
variableData[variable.name] = formData[variable.name];
|
||||
}
|
||||
const secrets = {};
|
||||
// ideally we would be able to force a template version here,
|
||||
// maybe rework backend types to force this in the API response
|
||||
// even if we don't need it in the config files
|
||||
const templateVersion: number = template.version || 0;
|
||||
const payload = {
|
||||
template_version: templateVersion,
|
||||
variables: variableData,
|
||||
secrets: secrets,
|
||||
};
|
||||
return payload;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { FileSourceTemplateSummary } from "@/api/fileSources";
|
||||
import type { UserConcreteObjectStore } from "@/components/ObjectStore/Instances/types";
|
||||
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
|
||||
|
||||
export const STANDARD_OBJECT_STORE_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: null,
|
||||
variables: [
|
||||
{
|
||||
name: "oldvar",
|
||||
type: "string",
|
||||
help: "old var help",
|
||||
},
|
||||
{
|
||||
name: "newvar",
|
||||
type: "string",
|
||||
help: "new var help",
|
||||
},
|
||||
],
|
||||
secrets: [
|
||||
{
|
||||
name: "oldsecret",
|
||||
help: "old secret help",
|
||||
},
|
||||
{
|
||||
name: "newsecret",
|
||||
help: "new secret help",
|
||||
},
|
||||
],
|
||||
id: "moo",
|
||||
version: 2,
|
||||
badges: [],
|
||||
};
|
||||
|
||||
export const STANDARD_FILE_SOURCE_TEMPLATE: FileSourceTemplateSummary = {
|
||||
type: "s3fs",
|
||||
name: "moo",
|
||||
description: null,
|
||||
variables: [
|
||||
{
|
||||
name: "oldvar",
|
||||
type: "string",
|
||||
help: "old var help",
|
||||
},
|
||||
{
|
||||
name: "newvar",
|
||||
type: "string",
|
||||
help: "new var help",
|
||||
},
|
||||
],
|
||||
secrets: [
|
||||
{
|
||||
name: "oldsecret",
|
||||
help: "old secret help",
|
||||
},
|
||||
{
|
||||
name: "newsecret",
|
||||
help: "new secret help",
|
||||
},
|
||||
],
|
||||
id: "moo",
|
||||
version: 2,
|
||||
};
|
||||
|
||||
export const GENERIC_FTP_FILE_SOURCE_TEMPLATE: FileSourceTemplateSummary = {
|
||||
id: "ftp",
|
||||
type: "ftp",
|
||||
name: "Generic FTP Server",
|
||||
description: "Generic FTP configuration with all configuration options exposed.",
|
||||
variables: [
|
||||
{ name: "host", label: "FTP Host", type: "string", help: "Host of FTP Server to connect to." },
|
||||
{
|
||||
name: "user",
|
||||
label: "FTP User",
|
||||
type: "string",
|
||||
help: "Username to login to target FTP server with.",
|
||||
},
|
||||
{
|
||||
name: "writable",
|
||||
label: "Writable?",
|
||||
type: "boolean",
|
||||
help: "Is this an FTP server you have permission to write to?",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "port",
|
||||
label: "FTP Port",
|
||||
type: "integer",
|
||||
help: "Port used to connect to the FTP server.",
|
||||
default: 21,
|
||||
},
|
||||
],
|
||||
secrets: [
|
||||
{
|
||||
name: "password",
|
||||
label: "FTP Password",
|
||||
help: "Password to connect to FTP server with.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const OBJECT_STORE_INSTANCE: UserConcreteObjectStore = {
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: undefined,
|
||||
template_id: "moo",
|
||||
template_version: 1,
|
||||
badges: [],
|
||||
variables: {
|
||||
oldvar: "my old value",
|
||||
droppedvar: "this will be dropped",
|
||||
},
|
||||
secrets: ["oldsecret", "droppedsecret"],
|
||||
quota: { enabled: false },
|
||||
private: false,
|
||||
id: 4,
|
||||
uuid: "112f889f-72d7-4619-a8e8-510a8c685aa7",
|
||||
active: true,
|
||||
hidden: false,
|
||||
purged: false,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { computed, type Ref } from "vue";
|
||||
|
||||
import { type Instance } from "@/api/configTemplates";
|
||||
|
||||
export function useFiltering<T extends Instance>(allInstances: Ref<T[]>) {
|
||||
const activeInstances = computed(() => {
|
||||
return allInstances.value.filter((item: T) => !item.hidden);
|
||||
});
|
||||
|
||||
return {
|
||||
activeInstances,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const MESSAGES = {
|
||||
posix: "This is a simple path based object store 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.",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
type: "posix" | "s3fs";
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const title = computed<string>(() => MESSAGES[props.type] ?? "");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-b-tooltip.hover class="file-source-type file-source-help-on-hover" :title="title">{{ type }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "./style.css";
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { FileSourceTemplateSummary, UserFileSourceModel } from "@/api/fileSources";
|
||||
import { createFormDataToPayload, createTemplateForm } from "@/components/ConfigTemplates/formUtil";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import { create } from "./services";
|
||||
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface CreateFormProps {
|
||||
template: FileSourceTemplateSummary;
|
||||
}
|
||||
const error = ref<string | null>(null);
|
||||
const props = defineProps<CreateFormProps>();
|
||||
const title = "Create a new file source for your data";
|
||||
const submitTitle = "Submit";
|
||||
const loadingMessage = "Loading file source template and instance information";
|
||||
|
||||
const inputs = computed(() => {
|
||||
return createTemplateForm(props.template, "file source");
|
||||
});
|
||||
|
||||
async function onSubmit(formData: any) {
|
||||
const payload = createFormDataToPayload(props.template, formData);
|
||||
try {
|
||||
const { data: fileSource } = await create(payload);
|
||||
emit("created", fileSource);
|
||||
} catch (e) {
|
||||
error.value = errorMessageAsString(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "created", fileSource: UserFileSourceModel): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div id="create-file-source-landing">
|
||||
<BAlert v-if="error" variant="danger" class="file-source-instance-creation-error" show>
|
||||
{{ error }}
|
||||
</BAlert>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type { UserFileSourceModel } from "@/api/fileSources";
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
import { useInstanceRouting } from "./routing";
|
||||
|
||||
import CreateForm from "@/components/FileSources/Instances/CreateForm.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
interface Props {
|
||||
templateId: string;
|
||||
}
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
fileSourceTemplatesStore.fetchTemplates();
|
||||
|
||||
const { goToIndex } = useInstanceRouting();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const template = computed(() => fileSourceTemplatesStore.getLatestTemplate(props.templateId));
|
||||
|
||||
async function onCreated(objectStore: UserFileSourceModel) {
|
||||
const message = `Created file source ${objectStore.name}`;
|
||||
goToIndex({ message });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LoadingSpan v-if="!template" message="Loading file source templates" />
|
||||
<CreateForm v-else :template="template" @created="onCreated"></CreateForm>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { BTab, BTabs } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { UserFileSourceModel } from "@/api/fileSources";
|
||||
import { editFormDataToPayload, editTemplateForm } from "@/components/ConfigTemplates/formUtil";
|
||||
|
||||
import { useInstanceAndTemplate } from "./instance";
|
||||
import { useInstanceRouting } from "./routing";
|
||||
import { update } from "./services";
|
||||
|
||||
import EditSecrets from "./EditSecrets.vue";
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface Props {
|
||||
instanceId: number | string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { instance, template } = useInstanceAndTemplate(ref(props.instanceId));
|
||||
|
||||
const inputs = computed(() => {
|
||||
template.value;
|
||||
instance.value;
|
||||
if (template.value && instance.value) {
|
||||
return editTemplateForm(template.value, "storage location", instance.value);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
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) {
|
||||
const payload = editFormDataToPayload(template.value, formData);
|
||||
const args = { user_file_source_id: String(instance?.value?.id) };
|
||||
const { data: fileSource } = await update({ ...args, ...payload });
|
||||
await onUpdate(fileSource);
|
||||
}
|
||||
}
|
||||
|
||||
const { goToIndex } = useInstanceRouting();
|
||||
|
||||
async function onUpdate(instance: UserFileSourceModel) {
|
||||
const message = `Updated file source ${instance.name}`;
|
||||
goToIndex({ message });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<BTabs v-if="hasSecrets">
|
||||
<BTab title="Settings" active>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</BTab>
|
||||
<BTab title="Secrets">
|
||||
<div v-if="instance && template">
|
||||
<EditSecrets :file-source="instance" :template="template" />
|
||||
</div>
|
||||
</BTab>
|
||||
</BTabs>
|
||||
<InstanceForm
|
||||
v-else
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type { FileSourceTemplateSummary, UserFileSourceModel } from "@/api/fileSources";
|
||||
|
||||
import { update } from "./services";
|
||||
|
||||
import EditSecretsForm from "@/components/ConfigTemplates/EditSecretsForm.vue";
|
||||
|
||||
interface Props {
|
||||
fileSource: UserFileSourceModel;
|
||||
template: FileSourceTemplateSummary;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const title = computed(() => `Update File Source ${props.fileSource?.name} Secrets`);
|
||||
|
||||
async function onUpdate(secretName: string, secretValue: string) {
|
||||
const payload = {
|
||||
secret_name: secretName,
|
||||
secret_value: secretValue,
|
||||
};
|
||||
const args = { user_file_source_id: String(props.fileSource.id) };
|
||||
await update({ ...args, ...payload });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<EditSecretsForm :title="title" :template="template" @update="onUpdate" />
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type { UserFileSourceModel } from "@/api/fileSources";
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
import InstanceDropdown from "@/components/ConfigTemplates/InstanceDropdown.vue";
|
||||
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
|
||||
interface Props {
|
||||
fileSource: UserFileSourceModel;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const routeEdit = computed(() => `/file_source_instances/${props.fileSource.id}/edit`);
|
||||
const routeUpgrade = computed(() => `/file_source_instances/${props.fileSource.id}/upgrade`);
|
||||
const isUpgradable = computed(() =>
|
||||
fileSourceTemplatesStore.canUpgrade(props.fileSource.template_id, props.fileSource.template_version)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InstanceDropdown
|
||||
prefix="file-source"
|
||||
:name="fileSource.name || ''"
|
||||
:is-upgradable="isUpgradable"
|
||||
:route-upgrade="routeUpgrade"
|
||||
:route-edit="routeEdit" />
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import "@/components/ConfigTemplates/icons";
|
||||
|
||||
import { BTable } from "bootstrap-vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
import { DESCRIPTION_FIELD, NAME_FIELD, TEMPLATE_FIELD, TYPE_FIELD } from "@/components/ConfigTemplates/fields";
|
||||
import { useFileSourceInstancesStore } from "@/stores/fileSourceInstancesStore";
|
||||
|
||||
import InstanceDropdown from "./InstanceDropdown.vue";
|
||||
import ManageIndexHeader from "@/components/ConfigTemplates/ManageIndexHeader.vue";
|
||||
import FileSourceTypeSpan from "@/components/FileSources/FileSourceTypeSpan.vue";
|
||||
import TemplateSummarySpan from "@/components/FileSources/Templates/TemplateSummarySpan.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
const fileSourceInstancesStore = useFileSourceInstancesStore();
|
||||
|
||||
interface Props {
|
||||
message: String | undefined | null;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const fields = [NAME_FIELD, DESCRIPTION_FIELD, TYPE_FIELD, TEMPLATE_FIELD];
|
||||
|
||||
const items = computed(() => fileSourceInstancesStore.getInstances);
|
||||
const loading = computed(() => fileSourceInstancesStore.loading);
|
||||
fileSourceInstancesStore.fetchInstances();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ManageIndexHeader
|
||||
:message="message"
|
||||
create-button-id="file-source-create"
|
||||
create-route="/file_source_instances/create">
|
||||
</ManageIndexHeader>
|
||||
<BTable
|
||||
id="user-file-sources-index"
|
||||
no-sort-reset
|
||||
:fields="fields"
|
||||
:items="items"
|
||||
:hover="true"
|
||||
:striped="true"
|
||||
:caption-top="true"
|
||||
:fixed="true"
|
||||
:show-empty="true">
|
||||
<template v-slot:empty>
|
||||
<LoadingSpan v-if="loading" message="Loading your user's file source instances" />
|
||||
<b-alert v-else id="no-file-source-instances" variant="info" show>
|
||||
<div>
|
||||
No file source instances found for your users, click the create button to configure a new one.
|
||||
</div>
|
||||
</b-alert>
|
||||
</template>
|
||||
<template v-slot:cell(name)="row">
|
||||
<InstanceDropdown :file-source="row.item" />
|
||||
</template>
|
||||
<template v-slot:cell(type)="row">
|
||||
<FileSourceTypeSpan :type="row.item.type" />
|
||||
</template>
|
||||
<template v-slot:cell(template)="row">
|
||||
<TemplateSummarySpan
|
||||
:template-version="row.item.template_version ?? 0"
|
||||
:template-id="row.item.template_id" />
|
||||
</template>
|
||||
</BTable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { FileSourceTemplateSummary, UserFileSourceModel } from "@/api/fileSources";
|
||||
import { upgradeForm, upgradeFormDataToPayload } from "@/components/ConfigTemplates/formUtil";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import { useInstanceRouting } from "./routing";
|
||||
import { update } from "./services";
|
||||
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface Props {
|
||||
instance: UserFileSourceModel;
|
||||
latestTemplate: FileSourceTemplateSummary;
|
||||
}
|
||||
|
||||
const error = ref<string | null>(null);
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const inputs = computed<Array<any> | null>(() => {
|
||||
const realizedInstance: UserFileSourceModel = props.instance;
|
||||
const realizedLatestTemplate = props.latestTemplate;
|
||||
const form = upgradeForm(realizedLatestTemplate, realizedInstance);
|
||||
return form;
|
||||
});
|
||||
const title = computed(() => `Upgrade File Source ${props.instance.name}`);
|
||||
const submitTitle = "Update Settings";
|
||||
const loadingMessage = "Loading file source template and instance information";
|
||||
|
||||
async function onSubmit(formData: any) {
|
||||
const payload = upgradeFormDataToPayload(props.latestTemplate, formData);
|
||||
const args = { user_file_source_id: String(props.instance.id) };
|
||||
try {
|
||||
const { data: fileSource } = await update({ ...args, ...payload });
|
||||
await onUpgrade(fileSource);
|
||||
} catch (e) {
|
||||
error.value = errorMessageAsString(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { goToIndex } = useInstanceRouting();
|
||||
|
||||
async function onUpgrade(fileSource: UserFileSourceModel) {
|
||||
const message = `Upgraded file source ${fileSource.name}`;
|
||||
goToIndex({ message });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<BAlert v-if="error" variant="danger" class="file-source-instance-upgrade-error" show>
|
||||
{{ error }}
|
||||
</BAlert>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
import { useInstanceAndTemplate } from "./instance";
|
||||
|
||||
import UpgradeForm from "./UpgradeForm.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
interface Props {
|
||||
instanceId: number | string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { instance } = useInstanceAndTemplate(ref(props.instanceId));
|
||||
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
|
||||
const latestTemplate = computed(
|
||||
() => instance.value && fileSourceTemplatesStore.getLatestTemplate(instance.value?.template_id)
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<LoadingSpan v-if="!instance || !latestTemplate" message="Loading file source instance and templates" />
|
||||
<UpgradeForm v-else :instance="instance" :latest-template="latestTemplate" />
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { computed, type Ref } from "vue";
|
||||
|
||||
import type { FileSourceTemplateSummary, UserFileSourceModel } from "@/api/fileSources";
|
||||
import { useFileSourceInstancesStore } from "@/stores/fileSourceInstancesStore";
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
export function useInstanceAndTemplate(instanceIdRef: Ref<string | number>) {
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
const fileSourceInstancesStore = useFileSourceInstancesStore();
|
||||
fileSourceInstancesStore.fetchInstances();
|
||||
fileSourceTemplatesStore.fetchTemplates();
|
||||
|
||||
const instance = computed<UserFileSourceModel | null>(
|
||||
() => fileSourceInstancesStore.getInstance(instanceIdRef.value) || null
|
||||
);
|
||||
const template = computed<FileSourceTemplateSummary | null>(() =>
|
||||
instance.value
|
||||
? fileSourceTemplatesStore.getTemplate(instance.value?.template_id, instance.value?.template_version)
|
||||
: null
|
||||
);
|
||||
|
||||
return { instance, template };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
export function useInstanceRouting() {
|
||||
const router = useRouter();
|
||||
|
||||
async function goToIndex(query: Record<"message", string>) {
|
||||
router.push({
|
||||
path: "/file_source_instances/index",
|
||||
query: query,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
goToIndex,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { fetcher } from "@/api/schema/fetcher";
|
||||
|
||||
export const create = fetcher.path("/api/file_source_instances").method("post").create();
|
||||
export const update = fetcher.path("/api/file_source_instances/{user_file_source_id}").method("put").create();
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
import SelectTemplate from "./SelectTemplate.vue";
|
||||
import CreateInstance from "@/components/ConfigTemplates/CreateInstance.vue";
|
||||
|
||||
const loadingTemplatesInfoMessage = "Loading file source templates";
|
||||
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
fileSourceTemplatesStore.ensureTemplates();
|
||||
|
||||
const templates = computed(() => fileSourceTemplatesStore.latestTemplates);
|
||||
const loading = computed(() => fileSourceTemplatesStore.loading);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function chooseTemplate(selectTemplateId: string) {
|
||||
router.push({
|
||||
path: `/file_source_templates/${selectTemplateId}/new`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<CreateInstance :loading-message="loadingTemplatesInfoMessage" :loading="loading" prefix="file-source">
|
||||
<SelectTemplate :templates="templates" @onSubmit="chooseTemplate" />
|
||||
</CreateInstance>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FileSourceTemplateSummaries } from "@/api/fileSources";
|
||||
|
||||
import TemplateSummaryPopover from "./TemplateSummaryPopover.vue";
|
||||
import SelectTemplate from "@/components/ConfigTemplates/SelectTemplate.vue";
|
||||
|
||||
interface Props {
|
||||
templates: FileSourceTemplateSummaries;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const selectText =
|
||||
"Select file source template to create new file sources with. These templates are configured by your Galaxy administrator.";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onSubmit", id: string): void;
|
||||
}>();
|
||||
|
||||
async function handleSubmit(templateId: string) {
|
||||
emit("onSubmit", templateId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SelectTemplate
|
||||
:templates="templates"
|
||||
:select-text="selectText"
|
||||
id-prefix="file-source"
|
||||
@onSubmit="handleSubmit">
|
||||
</SelectTemplate>
|
||||
<TemplateSummaryPopover
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
:target="`file-source-template-button-${template.id}`"
|
||||
:template="template" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import type { FileSourceTemplateSummary } from "@/api/fileSources";
|
||||
|
||||
import FileSourceTypeSpan from "@/components/FileSources/FileSourceTypeSpan.vue";
|
||||
import ConfigurationMarkdown from "@/components/ObjectStore/ConfigurationMarkdown.vue";
|
||||
|
||||
interface Props {
|
||||
template: FileSourceTemplateSummary;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const fileSourceType = computed(() => props.template.type);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>This template produces file sources of type <FileSourceTypeSpan :type="fileSourceType" />.</div>
|
||||
<ConfigurationMarkdown :markdown="template.description || ''" :admin="true" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FileSourceTemplateSummary } from "@/api/fileSources";
|
||||
|
||||
import TemplateSummary from "./TemplateSummary.vue";
|
||||
import TemplateSummaryPopover from "@/components/ConfigTemplates/TemplateSummaryPopover.vue";
|
||||
|
||||
interface Props {
|
||||
target: String;
|
||||
template: FileSourceTemplateSummary;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TemplateSummaryPopover :target="target" :template="template">
|
||||
<TemplateSummary :template="template" />
|
||||
</TemplateSummaryPopover>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
|
||||
import TemplateSummaryPopover from "./TemplateSummaryPopover.vue";
|
||||
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
|
||||
interface Props {
|
||||
templateId: string;
|
||||
templateVersion: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const template = computed(() => fileSourceTemplatesStore.getTemplate(props.templateId, props.templateVersion));
|
||||
const target = `template-summary-span-${crypto.randomUUID()}`;
|
||||
fileSourceTemplatesStore.ensureTemplates();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span v-if="template">
|
||||
<span :id="target">
|
||||
{{ template.name }}
|
||||
</span>
|
||||
<TemplateSummaryPopover :template="template" :target="target" />
|
||||
</span>
|
||||
<span v-else> Loading template information for {{ templateId }} {{ templateVersion }} </span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
.file-source-help-on-hover {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const FAKE_OBJECT_STORE = "A fake object store";
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
type: "s3",
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: null,
|
||||
variables: [
|
||||
@@ -36,7 +36,7 @@ const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
};
|
||||
|
||||
const SIMPLE_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
type: "s3",
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: null,
|
||||
variables: [
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { create } from "@/components/ObjectStore/Instances/services";
|
||||
import type { SecretData, UserConcreteObjectStore, VariableData } from "@/components/ObjectStore/Instances/types";
|
||||
import {
|
||||
metadataFormEntryDescription,
|
||||
metadataFormEntryName,
|
||||
templateSecretFormEntry,
|
||||
templateVariableFormEntry,
|
||||
} from "@/components/ObjectStore/Instances/util";
|
||||
import { createFormDataToPayload, createTemplateForm } from "@/components/ConfigTemplates/formUtil";
|
||||
import type { UserConcreteObjectStore } from "@/components/ObjectStore/Instances/types";
|
||||
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import InstanceForm from "./InstanceForm.vue";
|
||||
import { create } from "./services";
|
||||
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface CreateFormProps {
|
||||
template: ObjectStoreTemplateSummary;
|
||||
@@ -21,43 +18,14 @@ const error = ref<string | null>(null);
|
||||
const props = defineProps<CreateFormProps>();
|
||||
const title = "Create a new storage location for your data";
|
||||
const submitTitle = "Submit";
|
||||
const loadingMessage = "Loading storage location template and instance information";
|
||||
|
||||
const inputs = computed(() => {
|
||||
const form = [];
|
||||
const variables = props.template.variables ?? [];
|
||||
const secrets = props.template.secrets ?? [];
|
||||
form.push(metadataFormEntryName());
|
||||
form.push(metadataFormEntryDescription());
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, undefined));
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
form.push(templateSecretFormEntry(secret));
|
||||
}
|
||||
return form;
|
||||
return createTemplateForm(props.template, "storage location");
|
||||
});
|
||||
|
||||
async function onSubmit(formData: any) {
|
||||
const variables = props.template.variables ?? [];
|
||||
const secrets = props.template.secrets ?? [];
|
||||
const variableData: VariableData = {};
|
||||
const secretData: SecretData = {};
|
||||
for (const variable of variables) {
|
||||
variableData[variable.name] = formData[variable.name];
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
secretData[secret.name] = formData[secret.name];
|
||||
}
|
||||
const name: string = formData._meta_name;
|
||||
const description: string = formData._meta_description;
|
||||
const payload = {
|
||||
name: name,
|
||||
description: description,
|
||||
secrets: secretData,
|
||||
variables: variableData,
|
||||
template_id: props.template.id,
|
||||
template_version: props.template.version ?? 0,
|
||||
};
|
||||
const payload = createFormDataToPayload(props.template, formData);
|
||||
try {
|
||||
const { data: objectStore } = await create(payload);
|
||||
emit("created", objectStore);
|
||||
@@ -72,10 +40,15 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<b-alert v-if="error" variant="danger" class="object-store-instance-creation-error" show>
|
||||
<div id="create-object-store-landing">
|
||||
<BAlert v-if="error" variant="danger" class="object-store-instance-creation-error" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
<InstanceForm :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
|
||||
</BAlert>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { BTab, BTabs } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import {
|
||||
metadataFormEntryDescription,
|
||||
metadataFormEntryName,
|
||||
templateVariableFormEntry,
|
||||
} from "@/components/ObjectStore/Instances/util";
|
||||
import { editFormDataToPayload, editTemplateForm } from "@/components/ConfigTemplates/formUtil";
|
||||
|
||||
import { useInstanceAndTemplate } from "./instance";
|
||||
import { useInstanceRouting } from "./routing";
|
||||
import { update } from "./services";
|
||||
import type { UserConcreteObjectStore, VariableData } from "./types";
|
||||
import type { UserConcreteObjectStore } from "./types";
|
||||
|
||||
import EditSecrets from "./EditSecrets.vue";
|
||||
import InstanceForm from "./InstanceForm.vue";
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface Props {
|
||||
instanceId: number | string;
|
||||
@@ -23,47 +20,26 @@ const props = defineProps<Props>();
|
||||
const { instance, template } = useInstanceAndTemplate(ref(props.instanceId));
|
||||
|
||||
const inputs = computed(() => {
|
||||
if (!template.value || !instance.value) {
|
||||
return null;
|
||||
}
|
||||
const form = [];
|
||||
const nameInput = metadataFormEntryName();
|
||||
form.push({ value: instance?.value?.name ?? "", ...nameInput });
|
||||
|
||||
const descriptionInput = metadataFormEntryDescription();
|
||||
form.push({ value: instance?.value?.description ?? "", ...descriptionInput });
|
||||
|
||||
template.value;
|
||||
instance.value;
|
||||
if (template.value && instance.value) {
|
||||
const realizedInstance: UserConcreteObjectStore = instance.value;
|
||||
const variables = template.value?.variables ?? [];
|
||||
const variableValues: VariableData = realizedInstance.variables || {};
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, variableValues[variable.name]));
|
||||
}
|
||||
return editTemplateForm(template.value, "storage location", instance.value);
|
||||
}
|
||||
return form;
|
||||
return null;
|
||||
});
|
||||
|
||||
const title = computed(() => `Edit Storage Location ${instance.value?.name} Settings`);
|
||||
const hasSecrets = computed(() => instance.value?.secrets && instance.value?.secrets.length > 0);
|
||||
const submitTitle = "Update Settings";
|
||||
const loadingMessage = "Loading storage location template and instance information";
|
||||
|
||||
async function onSubmit(formData: any) {
|
||||
const variables = template?.value?.variables ?? [];
|
||||
const name = formData["_meta_name"];
|
||||
const description = formData["_meta_description"];
|
||||
const variableData: VariableData = {};
|
||||
for (const variable of variables) {
|
||||
variableData[variable.name] = formData[variable.name];
|
||||
if (template.value) {
|
||||
const payload = editFormDataToPayload(template.value, formData);
|
||||
const args = { user_object_store_id: String(instance?.value?.id) };
|
||||
const { data: objectStore } = await update({ ...args, ...payload });
|
||||
await onUpdate(objectStore);
|
||||
}
|
||||
const payload = {
|
||||
name: name,
|
||||
description: description,
|
||||
variables: variableData,
|
||||
};
|
||||
const args = { user_object_store_id: String(instance?.value?.id) };
|
||||
const { data: objectStore } = await update({ ...args, ...payload });
|
||||
await onUpdate(objectStore);
|
||||
}
|
||||
|
||||
const { goToIndex } = useInstanceRouting();
|
||||
@@ -75,16 +51,27 @@ async function onUpdate(objectStore: UserConcreteObjectStore) {
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<b-tabs v-if="hasSecrets">
|
||||
<b-tab title="Settings" active>
|
||||
<InstanceForm :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
|
||||
</b-tab>
|
||||
<b-tab title="Secrets">
|
||||
<BTabs v-if="hasSecrets">
|
||||
<BTab title="Settings" active>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</BTab>
|
||||
<BTab title="Secrets">
|
||||
<div v-if="instance && template">
|
||||
<EditSecrets :object-store="instance" :template="template" />
|
||||
</div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
<InstanceForm v-else :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
|
||||
</BTab>
|
||||
</BTabs>
|
||||
<InstanceForm
|
||||
v-else
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templa
|
||||
import { update } from "./services";
|
||||
import type { UserConcreteObjectStore } from "./types";
|
||||
|
||||
import VaultSecret from "./VaultSecret.vue";
|
||||
import FormCard from "@/components/Form/FormCard.vue";
|
||||
import EditSecretsForm from "@/components/ConfigTemplates/EditSecretsForm.vue";
|
||||
|
||||
interface Props {
|
||||
objectStore: UserConcreteObjectStore;
|
||||
@@ -26,14 +25,5 @@ async function onUpdate(secretName: string, secretValue: string) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<FormCard :title="title">
|
||||
<template v-slot:body>
|
||||
<div>
|
||||
<div v-for="secret in template.secrets" :key="secret.name">
|
||||
<VaultSecret :name="secret.name" :help="secret.help || ''" :is-set="true" @update="onUpdate">
|
||||
</VaultSecret>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormCard>
|
||||
<EditSecretsForm :title="title" :template="template" @update="onUpdate" />
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import "./icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { useObjectStoreTemplatesStore } from "@/stores/objectStoreTemplatesStore";
|
||||
|
||||
import { hide } from "./services";
|
||||
import type { UserConcreteObjectStore } from "./types";
|
||||
|
||||
const objectStoreTemplatesStore = useObjectStoreTemplatesStore();
|
||||
const router = useRouter();
|
||||
import InstanceDropdown from "@/components/ConfigTemplates/InstanceDropdown.vue";
|
||||
|
||||
// TODO?
|
||||
const title = "";
|
||||
const objectStoreTemplatesStore = useObjectStoreTemplatesStore();
|
||||
|
||||
interface Props {
|
||||
objectStore: UserConcreteObjectStore;
|
||||
@@ -25,33 +20,19 @@ const routeUpgrade = computed(() => `/object_store_instances/${props.objectStore
|
||||
const isUpgradable = computed(() =>
|
||||
objectStoreTemplatesStore.canUpgrade(props.objectStore.template_id, props.objectStore.template_version)
|
||||
);
|
||||
|
||||
async function onRemove() {
|
||||
await hide(props.objectStore);
|
||||
console.log("HIDING!!!");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-link
|
||||
v-b-tooltip.hover
|
||||
class="object-store-instance-dropdown font-weight-bold"
|
||||
data-toggle="dropdown"
|
||||
:title="title"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<FontAwesomeIcon icon="caret-down" />
|
||||
<span class="instance-dropdown-name">{{ props.objectStore.name }}</span>
|
||||
</b-link>
|
||||
<div class="dropdown-menu" aria-labelledby="object-store-instance-dropdown">
|
||||
<a
|
||||
v-if="isUpgradable"
|
||||
class="dropdown-item"
|
||||
@keypress="router.push(routeUpgrade)"
|
||||
@click.prevent="router.push(routeUpgrade)">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Upgrade</span>
|
||||
</a>
|
||||
<a class="dropdown-item" @keypress="router.push(routeEdit)" @click.prevent="router.push(routeEdit)">
|
||||
<span class="fa fa-edit fa-fw mr-1" />
|
||||
<span v-localize>Edit configuration</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<InstanceDropdown
|
||||
prefix="object-store"
|
||||
:name="objectStore.name || ''"
|
||||
:is-upgradable="isUpgradable"
|
||||
:route-upgrade="routeUpgrade"
|
||||
:route-edit="routeEdit"
|
||||
@remove="onRemove" />
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import "./icons";
|
||||
import "@/components/ConfigTemplates/icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BTable } from "bootstrap-vue";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import type { UserConcreteObjectStore } from "@/api/objectStores";
|
||||
import { DESCRIPTION_FIELD, NAME_FIELD, TEMPLATE_FIELD, TYPE_FIELD } from "@/components/ConfigTemplates/fields";
|
||||
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";
|
||||
import ObjectStoreTypeSpan from "@/components/ObjectStore/ObjectStoreTypeSpan.vue";
|
||||
import TemplateSummarySpan from "@/components/ObjectStore/Templates/TemplateSummarySpan.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const objectStoreInstancesStore = useObjectStoreInstancesStore();
|
||||
|
||||
interface Props {
|
||||
@@ -23,59 +25,32 @@ interface Props {
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: "name",
|
||||
label: _l("Name"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "description",
|
||||
label: _l("Description"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
label: _l("Type"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "template",
|
||||
label: _l("From Template"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "badges",
|
||||
label: _l(" "),
|
||||
sortable: false,
|
||||
},
|
||||
];
|
||||
const BADGE_FIELD = {
|
||||
key: "badges",
|
||||
label: _l(" "),
|
||||
sortable: false,
|
||||
};
|
||||
|
||||
const items = computed(() => objectStoreInstancesStore.getInstances);
|
||||
const fields = [NAME_FIELD, DESCRIPTION_FIELD, TYPE_FIELD, TEMPLATE_FIELD, BADGE_FIELD];
|
||||
|
||||
const allItems = computed<UserConcreteObjectStore[]>(() => objectStoreInstancesStore.getInstances);
|
||||
const { activeInstances } = useFiltering(allItems);
|
||||
const loading = computed(() => objectStoreInstancesStore.loading);
|
||||
objectStoreInstancesStore.fetchInstances();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
{{ message || "" }}
|
||||
</p>
|
||||
<b-row class="mb-3">
|
||||
<b-col>
|
||||
<b-button
|
||||
id="object-store-create"
|
||||
class="m-1 float-right"
|
||||
@click="router.push('/object_store_instances/create')">
|
||||
<FontAwesomeIcon icon="plus" />
|
||||
{{ _l("Create") }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-table
|
||||
<ManageIndexHeader
|
||||
:message="message"
|
||||
create-button-id="object-store-create"
|
||||
create-route="/object_store_instances/create">
|
||||
</ManageIndexHeader>
|
||||
<BTable
|
||||
id="user-object-stores-index"
|
||||
no-sort-reset
|
||||
:fields="fields"
|
||||
:items="items"
|
||||
:items="activeInstances"
|
||||
:hover="true"
|
||||
:striped="true"
|
||||
:caption-top="true"
|
||||
@@ -101,6 +76,6 @@ objectStoreInstancesStore.fetchInstances();
|
||||
:template-version="row.item.template_version ?? 0"
|
||||
:template-id="row.item.template_id" />
|
||||
</template>
|
||||
</b-table>
|
||||
</BTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@ const localVue = getLocalVue(true);
|
||||
const router = injectTestRouter(localVue);
|
||||
|
||||
const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
type: "s3",
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: null,
|
||||
variables: [
|
||||
@@ -46,7 +46,7 @@ const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
|
||||
};
|
||||
|
||||
const INSTANCE: UserConcreteObjectStore = {
|
||||
type: "s3",
|
||||
type: "aws_s3",
|
||||
name: "moo",
|
||||
description: undefined,
|
||||
template_id: "moo",
|
||||
@@ -61,6 +61,9 @@ const INSTANCE: UserConcreteObjectStore = {
|
||||
private: false,
|
||||
id: 4,
|
||||
uuid: "112f889f-72d7-4619-a8e8-510a8c685aa7",
|
||||
active: true,
|
||||
hidden: false,
|
||||
purged: false,
|
||||
};
|
||||
|
||||
describe("UpgradeForm", () => {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { upgradeForm, upgradeFormDataToPayload } from "@/components/ConfigTemplates/formUtil";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import type { ObjectStoreTemplateSummary } from "../Templates/types";
|
||||
import { useInstanceRouting } from "./routing";
|
||||
import { update } from "./services";
|
||||
import type { UserConcreteObjectStore, VariableData } from "./types";
|
||||
import { templateSecretFormEntry, templateVariableFormEntry } from "./util";
|
||||
import type { UserConcreteObjectStore } from "./types";
|
||||
|
||||
import InstanceForm from "./InstanceForm.vue";
|
||||
import InstanceForm from "@/components/ConfigTemplates/InstanceForm.vue";
|
||||
|
||||
interface Props {
|
||||
instance: UserConcreteObjectStore;
|
||||
@@ -20,45 +21,17 @@ const error = ref<string | null>(null);
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const inputs = computed<Array<any> | null>(() => {
|
||||
const form = [];
|
||||
const realizedInstance: UserConcreteObjectStore = props.instance;
|
||||
const realizedLatestTemplate = props.latestTemplate;
|
||||
const variables = realizedLatestTemplate.variables ?? [];
|
||||
const secrets = realizedLatestTemplate.secrets ?? [];
|
||||
const variableValues: VariableData = realizedInstance.variables || {};
|
||||
const secretsSet = realizedInstance.secrets || [];
|
||||
for (const variable of variables) {
|
||||
form.push(templateVariableFormEntry(variable, variableValues[variable.name]));
|
||||
}
|
||||
for (const secret of secrets) {
|
||||
const secretName = secret.name;
|
||||
if (secretsSet.indexOf(secretName) >= 0) {
|
||||
console.log("skipping...");
|
||||
} else {
|
||||
form.push(templateSecretFormEntry(secret));
|
||||
}
|
||||
}
|
||||
const form = upgradeForm(realizedLatestTemplate, realizedInstance);
|
||||
return form;
|
||||
});
|
||||
const title = computed(() => `Upgrade Object Store ${props.instance.name}`);
|
||||
const submitTitle = "Update Settings";
|
||||
const loadingMessage = "Loading storage location template and instance information";
|
||||
|
||||
async function onSubmit(formData: any) {
|
||||
const variables = props.latestTemplate.variables ?? [];
|
||||
const variableData: VariableData = {};
|
||||
for (const variable of variables) {
|
||||
variableData[variable.name] = formData[variable.name];
|
||||
}
|
||||
const secrets = {};
|
||||
// ideally we would be able to force a template version here,
|
||||
// maybe rework backend types to force this in the API response
|
||||
// even if we don't need it in the config files
|
||||
const templateVersion: number = props.latestTemplate.version || 0;
|
||||
const payload = {
|
||||
template_version: templateVersion,
|
||||
variables: variableData,
|
||||
secrets: secrets,
|
||||
};
|
||||
const payload = upgradeFormDataToPayload(props.latestTemplate, formData);
|
||||
const args = { user_object_store_id: String(props.instance.id) };
|
||||
try {
|
||||
const { data: objectStore } = await update({ ...args, ...payload });
|
||||
@@ -78,9 +51,14 @@ async function onUpgrade(objectStore: UserConcreteObjectStore) {
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<b-alert v-if="error" variant="danger" class="object-store-instance-upgrade-error" show>
|
||||
<BAlert v-if="error" variant="danger" class="object-store-instance-upgrade-error" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
<InstanceForm :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
|
||||
</BAlert>
|
||||
<InstanceForm
|
||||
:inputs="inputs"
|
||||
:title="title"
|
||||
:submit-title="submitTitle"
|
||||
:loading-message="loadingMessage"
|
||||
@onSubmit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { fetcher } from "@/api/schema/fetcher";
|
||||
|
||||
import type { UserConcreteObjectStore } from "./types";
|
||||
|
||||
export const create = fetcher.path("/api/object_store_instances").method("post").create();
|
||||
export const update = fetcher.path("/api/object_store_instances/{user_object_store_id}").method("put").create();
|
||||
|
||||
export async function hide(instance: UserConcreteObjectStore) {
|
||||
const payload = { hidden: true };
|
||||
const args = { user_object_store_id: String(instance?.id) };
|
||||
const { data: objectStore } = await update({ ...args, ...payload });
|
||||
return objectStore;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,3 @@ import type { components } from "@/api/schema/schema";
|
||||
|
||||
export type UserConcreteObjectStore = components["schemas"]["UserConcreteObjectStoreModel"];
|
||||
export type CreateInstancePayload = components["schemas"]["CreateInstancePayload"];
|
||||
export type ObjectStoreTemplateVariable = components["schemas"]["ObjectStoreTemplateVariable"];
|
||||
export type ObjectStoreTemplateSecret = components["schemas"]["ObjectStoreTemplateSecret"];
|
||||
export type VariableValueType = (string | boolean | number) | undefined;
|
||||
export type VariableData = { [key: string]: VariableValueType };
|
||||
export type SecretData = { [key: string]: string };
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { markup } from "@/components/ObjectStore/configurationMarkdown";
|
||||
|
||||
import type { ObjectStoreTemplateSecret, ObjectStoreTemplateVariable, VariableValueType } from "./types";
|
||||
|
||||
export function metadataFormEntryName() {
|
||||
return {
|
||||
name: "_meta_name",
|
||||
label: "Name",
|
||||
type: "text",
|
||||
optional: false,
|
||||
help: "Label this new object store a name.",
|
||||
};
|
||||
}
|
||||
|
||||
export function metadataFormEntryDescription() {
|
||||
return {
|
||||
name: "_meta_description",
|
||||
label: "Description",
|
||||
optional: true,
|
||||
type: "textarea",
|
||||
help: "Provide some notes to yourself about this object store - perhaps to remind you how it is configured, where it stores the data, etc..",
|
||||
};
|
||||
}
|
||||
|
||||
export function templateVariableFormEntry(variable: ObjectStoreTemplateVariable, variableValue: VariableValueType) {
|
||||
return {
|
||||
name: variable.name,
|
||||
type: "text",
|
||||
help: markup(variable.help || "", true),
|
||||
value: variableValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function templateSecretFormEntry(secret: ObjectStoreTemplateSecret) {
|
||||
return {
|
||||
name: secret.name,
|
||||
type: "password",
|
||||
help: markup(secret.help || "", true),
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { ObjectStoreTemplateType } from "@/api/objectStores";
|
||||
|
||||
const MESSAGES = {
|
||||
s3: "This is an object store based on the Amazon Simple Storage Service (S3). Data here is hosted by Amazon.",
|
||||
aws_s3: "This is an object store based on the Amazon Simple Storage Service (S3). Data here is hosted by Amazon.",
|
||||
azure_blob:
|
||||
"This is a Microsoft Azure Blob based object store. More information on Microsoft's Azure Blob Storage can be found at https://azure.microsoft.com/en-us/products/storage/blobs/.",
|
||||
disk: "This is a simple path based object store that assumes the all the relevant paths are already mounted on the Galaxy server and target worker nodes.",
|
||||
boto3: "This is an object store based on the Amazon Simple Storage Service (S3) interface, but likely not stored by Amazon. The AWS interface has become an industry standard and many storage vendors support it and use it to expose object based storage.",
|
||||
generic_s3:
|
||||
"This is an object store based on the Amazon Simple Storage Service (S3) interface, but likely not stored by Amazon. The AWS interface has become an industry standard and many storage vendors support it and use it to expose object based storage.",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
type: "s3" | "azure_blob" | "generic_s3" | "disk";
|
||||
type: ObjectStoreTemplateType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -28,6 +28,17 @@ const store = useObjectStoreStore();
|
||||
const { isLoading, loadErrorMessage, selectableObjectStores } = storeToRefs(store);
|
||||
const { isOnlyPreference } = useStorageLocationConfiguration();
|
||||
|
||||
const selectableAndVisibleObjectStores = computed(() => {
|
||||
const allSelectableObjectStores = selectableObjectStores.value;
|
||||
if (allSelectableObjectStores != null) {
|
||||
return allSelectableObjectStores.filter((item) => {
|
||||
return "hidden" in item ? !item.hidden : true;
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const loadingObjectStoreInfoMessage = ref("Loading storage location information");
|
||||
const whyIsSelectionPreferredText = ref(`
|
||||
Select a preferred storage location for new datasets. Depending on the job and workflow execution configuration of
|
||||
@@ -78,7 +89,7 @@ async function handleSubmit(preferredObjectStore: ConcreteObjectStoreModel | nul
|
||||
><i>{{ defaultOptionTitle | localize }}</i></b-button
|
||||
>
|
||||
<ObjectStoreSelectButton
|
||||
v-for="objectStore in selectableObjectStores"
|
||||
v-for="objectStore in selectableAndVisibleObjectStores"
|
||||
:key="objectStore.object_store_id"
|
||||
id-prefix="preferred"
|
||||
:object-store="objectStore"
|
||||
@@ -97,7 +108,7 @@ async function handleSubmit(preferredObjectStore: ConcreteObjectStoreModel | nul
|
||||
<span v-localize>{{ defaultOptionDescription }}</span>
|
||||
</ObjectStoreSelectButtonPopover>
|
||||
<ObjectStoreSelectButtonDescribePopover
|
||||
v-for="objectStore in selectableObjectStores"
|
||||
v-for="objectStore in selectableAndVisibleObjectStores"
|
||||
:key="objectStore.object_store_id"
|
||||
id-prefix="preferred"
|
||||
:what="forWhat"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { useObjectStoreTemplatesStore } from "@/stores/objectStoreTemplatesStore";
|
||||
|
||||
import SelectTemplate from "./SelectTemplate.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
import CreateInstance from "@/components/ConfigTemplates/CreateInstance.vue";
|
||||
|
||||
const loadingTemplatesInfoMessage = "Loading storage location templates";
|
||||
|
||||
@@ -17,8 +17,6 @@ const loading = computed(() => objectStoreTemplatesStore.loading);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function chooseTemplate(selectTemplateId: string) {
|
||||
router.push({
|
||||
path: `/object_store_templates/${selectTemplateId}/new`,
|
||||
@@ -26,14 +24,7 @@ async function chooseTemplate(selectTemplateId: string) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<b-container fluid class="p-0">
|
||||
<LoadingSpan v-if="loading" :message="loadingTemplatesInfoMessage" />
|
||||
<div v-else>
|
||||
<b-alert v-if="error" variant="danger" class="object-store-selection-error" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
|
||||
<SelectTemplate :templates="templates" @onSubmit="chooseTemplate" />
|
||||
</div>
|
||||
</b-container>
|
||||
<CreateInstance :loading-message="loadingTemplatesInfoMessage" :loading="loading" prefix="object-store">
|
||||
<SelectTemplate :templates="templates" @onSubmit="chooseTemplate" />
|
||||
</CreateInstance>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ObjectStoreTemplateSummaries } from "./types";
|
||||
|
||||
import TemplateSummaryPopover from "./TemplateSummaryPopover.vue";
|
||||
import SelectTemplate from "@/components/ConfigTemplates/SelectTemplate.vue";
|
||||
|
||||
interface SelectTemplateProps {
|
||||
templates: ObjectStoreTemplateSummaries;
|
||||
@@ -23,26 +24,12 @@ async function handleSubmit(templateId: string) {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col cols="7">
|
||||
<b-button-group vertical size="lg" style="width: 100%">
|
||||
<b-button
|
||||
v-for="template in templates"
|
||||
:id="`object-store-template-button-${template.id}`"
|
||||
:key="template.id"
|
||||
class="object-store-template-select-button"
|
||||
:data-template-id="template.id"
|
||||
@click="handleSubmit(template.id)"
|
||||
>{{ template.name }}
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</b-col>
|
||||
<b-col cols="5">
|
||||
<p v-localize style="float: right">
|
||||
{{ selectText }}
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<SelectTemplate
|
||||
:templates="templates"
|
||||
:select-text="selectText"
|
||||
id-prefix="object-store"
|
||||
@onSubmit="handleSubmit">
|
||||
</SelectTemplate>
|
||||
<TemplateSummaryPopover
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
|
||||
@@ -22,7 +22,7 @@ const objectStoreType = computed(() => props.template.type);
|
||||
<template>
|
||||
<div>
|
||||
<ObjectStoreBadges :badges="badges" size="lg" />
|
||||
<div>This template produces object stores of type <ObjectStoreTypeSpan :type="objectStoreType" />.</div>
|
||||
<div>This template produces storage locations of type <ObjectStoreTypeSpan :type="objectStoreType" />.</div>
|
||||
<ConfigurationMarkdown :markdown="template.description || ''" :admin="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { STANDARD_OBJECT_STORE_TEMPLATE } from "@/components/ConfigTemplates/test_fixtures";
|
||||
|
||||
import TemplateSummaryPopover from "./TemplateSummaryPopover.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("TemplateSummaryPopover", () => {
|
||||
it("should render a popover", async () => {
|
||||
const wrapper = shallowMount(TemplateSummaryPopover, {
|
||||
propsData: {
|
||||
target: "test-target-1",
|
||||
template: STANDARD_OBJECT_STORE_TEMPLATE,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
const configPopover = wrapper.find("[target='test-target-1']");
|
||||
expect(configPopover.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,18 @@
|
||||
import type { ObjectStoreTemplateSummary } from "./types";
|
||||
|
||||
import TemplateSummary from "./TemplateSummary.vue";
|
||||
import TemplateSummaryPopover from "@/components/ConfigTemplates/TemplateSummaryPopover.vue";
|
||||
|
||||
interface Props {
|
||||
target: String;
|
||||
template: ObjectStoreTemplateSummary;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const popoverPlacement = "rightbottom";
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-popover :target="target" triggers="hover" boundary="window" :placement="popoverPlacement">
|
||||
<template v-slot:title>{{ props.template.name }}</template>
|
||||
<TemplateSummary :template="props.template" />
|
||||
</b-popover>
|
||||
<TemplateSummaryPopover :target="target" :template="template">
|
||||
<TemplateSummary :template="template" />
|
||||
</TemplateSummaryPopover>
|
||||
</template>
|
||||
|
||||
@@ -93,13 +93,21 @@
|
||||
:user-id="userId">
|
||||
</UserPreferredObjectStore>
|
||||
<UserPreferencesElement
|
||||
v-if="hasTemplates"
|
||||
v-if="hasObjectStoreTemplates"
|
||||
id="manage-object-stores"
|
||||
class="manage-object-stores"
|
||||
icon="fa-hdd"
|
||||
title="Manage Your Storage Locations"
|
||||
description="Add, remove, or update your personally configured storage locations."
|
||||
to="/object_store_instances/index" />
|
||||
<UserPreferencesElement
|
||||
v-if="hasFileSourceTemplates"
|
||||
id="manage-file-sources"
|
||||
class="manage-file-sources"
|
||||
icon="fa-file"
|
||||
title="Manage Your Remote File Sources"
|
||||
description="Add, remove, or update your personally configured location to find files from and write files to."
|
||||
to="/file_source_instances/index" />
|
||||
<UserDeletion
|
||||
v-if="isConfigLoaded && !config.single_user && config.enable_account_interface"
|
||||
:email="email"
|
||||
@@ -144,6 +152,7 @@ import { withPrefix } from "utils/redirect";
|
||||
import Vue from "vue";
|
||||
|
||||
import { useConfig } from "@/composables/config";
|
||||
import { useFileSourceTemplatesStore } from "@/stores/fileSourceTemplatesStore";
|
||||
import { useObjectStoreTemplatesStore } from "@/stores/objectStoreTemplatesStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
|
||||
@@ -192,7 +201,12 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(useUserStore, ["currentUser"]),
|
||||
...mapState(useObjectStoreTemplatesStore, ["hasTemplates"]),
|
||||
...mapState(useObjectStoreTemplatesStore, {
|
||||
hasObjectStoreTemplates: "hasTemplates",
|
||||
}),
|
||||
...mapState(useFileSourceTemplatesStore, {
|
||||
hasFileSourceTemplates: "hasTemplates",
|
||||
}),
|
||||
activePreferences() {
|
||||
const userPreferencesEntries = Object.entries(getUserPreferencesModel());
|
||||
// Object.entries returns an array of arrays, where the first element
|
||||
@@ -232,10 +246,16 @@ export default {
|
||||
this.diskUsage = response.data.nice_total_disk_usage;
|
||||
this.diskQuota = response.data.quota;
|
||||
});
|
||||
this.ensureTemplates();
|
||||
this.ensureObjectStoreTemplates();
|
||||
this.ensureFileSourceTemplates();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useObjectStoreTemplatesStore, ["ensureTemplates"]),
|
||||
...mapActions(useObjectStoreTemplatesStore, {
|
||||
ensureObjectStoreTemplates: "ensureTemplates",
|
||||
}),
|
||||
...mapActions(useFileSourceTemplatesStore, {
|
||||
ensureFileSourceTemplates: "ensureTemplates",
|
||||
}),
|
||||
toggleNotifications() {
|
||||
if (window.Notification) {
|
||||
Notification.requestPermission().then(function (permission) {
|
||||
|
||||
@@ -55,22 +55,27 @@ import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
|
||||
import AvailableDatatypes from "@/components/AvailableDatatypes/AvailableDatatypes";
|
||||
import CreateFileSourceInstance from "@/components/FileSources/Instances/CreateInstance";
|
||||
import GridHistory from "@/components/Grid/GridHistory";
|
||||
import GridPage from "@/components/Grid/GridPage";
|
||||
import CreateInstance from "@/components/ObjectStore/Instances/CreateInstance";
|
||||
import CreateObjectStoreInstance from "@/components/ObjectStore/Instances/CreateInstance";
|
||||
import { parseBool } from "@/utils/utils";
|
||||
|
||||
import { patchRouterPush } from "./router-push";
|
||||
|
||||
import AboutGalaxy from "@/components/AboutGalaxy.vue";
|
||||
import EditFileSourceInstance from "@/components/FileSources/Instances/EditInstance.vue";
|
||||
import ManageFileSourceIndex from "@/components/FileSources/Instances/ManageIndex.vue";
|
||||
import UpgradeFileSourceInstance from "@/components/FileSources/Instances/UpgradeInstance.vue";
|
||||
import CreateUserFileSource from "@/components/FileSources/Templates/CreateUserFileSource.vue";
|
||||
import GridInvocation from "@/components/Grid/GridInvocation.vue";
|
||||
import GridVisualization from "@/components/Grid/GridVisualization.vue";
|
||||
import HistoryArchiveWizard from "@/components/History/Archiving/HistoryArchiveWizard.vue";
|
||||
import HistoryDatasetPermissions from "@/components/History/HistoryDatasetPermissions.vue";
|
||||
import NotificationsList from "@/components/Notifications/NotificationsList.vue";
|
||||
import EditInstance from "@/components/ObjectStore/Instances/EditInstance.vue";
|
||||
import ManageIndex from "@/components/ObjectStore/Instances/ManageIndex.vue";
|
||||
import UpgradeInstance from "@/components/ObjectStore/Instances/UpgradeInstance.vue";
|
||||
import EditObjectStoreInstance from "@/components/ObjectStore/Instances/EditInstance.vue";
|
||||
import ManageObjectStoreIndex from "@/components/ObjectStore/Instances/ManageIndex.vue";
|
||||
import UpgradeObjectStoreInstance from "@/components/ObjectStore/Instances/UpgradeInstance.vue";
|
||||
import CreateUserObjectStore from "@/components/ObjectStore/Templates/CreateUserObjectStore.vue";
|
||||
import Sharing from "@/components/Sharing/SharingPage.vue";
|
||||
import HistoryStorageOverview from "@/components/User/DiskUsage/Visualizations/HistoryStorageOverview.vue";
|
||||
@@ -356,24 +361,50 @@ export function getRouter(Galaxy) {
|
||||
},
|
||||
{
|
||||
path: "object_store_instances/index",
|
||||
component: ManageIndex,
|
||||
component: ManageObjectStoreIndex,
|
||||
props: (route) => {
|
||||
return { message: route.query["message"] };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "object_store_instances/:instanceId/edit",
|
||||
component: EditInstance,
|
||||
component: EditObjectStoreInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "object_store_instances/:instanceId/upgrade",
|
||||
component: UpgradeInstance,
|
||||
component: UpgradeObjectStoreInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "object_store_templates/:templateId/new",
|
||||
component: CreateInstance,
|
||||
component: CreateObjectStoreInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "file_source_instances/create",
|
||||
component: CreateUserFileSource,
|
||||
},
|
||||
{
|
||||
path: "file_source_instances/index",
|
||||
component: ManageFileSourceIndex,
|
||||
props: (route) => {
|
||||
return { message: route.query["message"] };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "file_source_instances/:instanceId/edit",
|
||||
component: EditFileSourceInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "file_source_instances/:instanceId/upgrade",
|
||||
component: UpgradeFileSourceInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "file_source_templates/:templateId/new",
|
||||
component: CreateFileSourceInstance,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { TemplateSummary } from "@/api/configTemplates";
|
||||
|
||||
export function findTemplate<T extends TemplateSummary>(
|
||||
templates: T[],
|
||||
templateId: string,
|
||||
templateVersion: number
|
||||
): T | null {
|
||||
for (const template of templates) {
|
||||
if (template.id == templateId && template.version == templateVersion) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLatestVersionMap<T extends TemplateSummary>(templates: T[]): { [key: string]: number } {
|
||||
const latestVersions: { [key: string]: number } = {};
|
||||
templates.forEach((i: T) => {
|
||||
const templateId = i.id;
|
||||
const templateVersion = i.version || 0;
|
||||
if ((latestVersions[templateId] ?? -1) < templateVersion) {
|
||||
latestVersions[templateId] = templateVersion;
|
||||
}
|
||||
});
|
||||
return latestVersions;
|
||||
}
|
||||
|
||||
export function canUpgrade<T extends TemplateSummary>(
|
||||
templates: T[],
|
||||
templateId: string,
|
||||
templateVersion: number
|
||||
): boolean {
|
||||
let can = false;
|
||||
templates.forEach((i: T) => {
|
||||
if (i.id == templateId && i.version && i.version > templateVersion) {
|
||||
can = true;
|
||||
}
|
||||
});
|
||||
return can;
|
||||
}
|
||||
|
||||
export function getLatestVersion<T extends TemplateSummary>(templates: T[], id: string): T | null {
|
||||
let latestVersion = -1;
|
||||
let latestTemplate = null as T | null;
|
||||
templates.forEach((i: T) => {
|
||||
const templateId = i.id;
|
||||
if (templateId == id) {
|
||||
const templateVersion = i.version || 0;
|
||||
if (templateVersion > latestVersion) {
|
||||
latestTemplate = i;
|
||||
latestVersion = templateVersion;
|
||||
}
|
||||
}
|
||||
});
|
||||
return latestTemplate;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { fetcher } from "@/api/schema/fetcher";
|
||||
import type { components } from "@/api/schema/schema";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
const getFileSourceInstances = fetcher.path("/api/file_source_instances").method("get").create();
|
||||
|
||||
type UserFileSourceModel = components["schemas"]["UserFileSourceModel"];
|
||||
|
||||
export const useFileSourceInstancesStore = defineStore("fileSourceInstances", {
|
||||
state: () => ({
|
||||
instances: [] as UserFileSourceModel[],
|
||||
fetched: false,
|
||||
error: null as string | null,
|
||||
}),
|
||||
getters: {
|
||||
getInstances: (state) => {
|
||||
return state.instances;
|
||||
},
|
||||
loading: (state) => {
|
||||
return !state.fetched;
|
||||
},
|
||||
getInstance: (state) => {
|
||||
return (id: number | string) => state.instances.find((i) => i.id.toString() == id.toString());
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async handleInit(instances: UserFileSourceModel[]) {
|
||||
this.instances = instances;
|
||||
this.fetched = true;
|
||||
this.error = null;
|
||||
},
|
||||
async handleError(err: unknown) {
|
||||
this.error = errorMessageAsString(err);
|
||||
},
|
||||
async fetchInstances() {
|
||||
try {
|
||||
const { data: instances } = await getFileSourceInstances({});
|
||||
this.handleInit(instances);
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
}
|
||||
},
|
||||
async ensureTemplates() {
|
||||
if (!this.fetched || this.error != null) {
|
||||
await this.fetchInstances();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { fetcher } from "@/api/schema/fetcher";
|
||||
import type { components } from "@/api/schema/schema";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import { canUpgrade, findTemplate, getLatestVersion, getLatestVersionMap } from "./configTemplatesUtil";
|
||||
|
||||
const getFileSourceTemplates = fetcher.path("/api/file_source_templates").method("get").create();
|
||||
|
||||
type FileSourceTemplateSummary = components["schemas"]["FileSourceTemplateSummary"];
|
||||
type FileSourceTemplateSummaries = FileSourceTemplateSummary[];
|
||||
|
||||
export const useFileSourceTemplatesStore = defineStore("fileSourceTemplatesStore", {
|
||||
state: () => ({
|
||||
templates: [] as FileSourceTemplateSummaries,
|
||||
fetched: false,
|
||||
error: null as string | null,
|
||||
}),
|
||||
getters: {
|
||||
latestTemplates: (state) => {
|
||||
// only expose latest instance by template_version for each template_id
|
||||
const latestVersions = getLatestVersionMap(state.templates);
|
||||
return state.templates.filter((i: FileSourceTemplateSummary) => latestVersions[i.id] == (i.version || 0));
|
||||
},
|
||||
canUpgrade: (state) => {
|
||||
return (templateId: string, templateVersion: number) =>
|
||||
canUpgrade(state.templates, templateId, templateVersion);
|
||||
},
|
||||
getTemplates: (state) => {
|
||||
return state.templates;
|
||||
},
|
||||
getTemplate: (state) => {
|
||||
return (templateId: string, templateVersion: number) =>
|
||||
findTemplate(state.templates, templateId, templateVersion);
|
||||
},
|
||||
getLatestTemplate: (state) => {
|
||||
return (templateId: string) => getLatestVersion(state.templates, templateId);
|
||||
},
|
||||
hasTemplates: (state) => {
|
||||
return state.templates.length > 0;
|
||||
},
|
||||
loading: (state) => {
|
||||
return !state.fetched;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async handleInit(templates: FileSourceTemplateSummaries) {
|
||||
this.templates = templates;
|
||||
this.fetched = true;
|
||||
},
|
||||
async handleError(err: unknown) {
|
||||
this.fetched = true;
|
||||
this.error = errorMessageAsString(err);
|
||||
},
|
||||
async fetchTemplates() {
|
||||
try {
|
||||
const { data: templates } = await getFileSourceTemplates({});
|
||||
this.handleInit(templates);
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
}
|
||||
},
|
||||
async ensureTemplates() {
|
||||
if (!this.fetched || this.error != null) {
|
||||
await this.fetchTemplates();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type ObjectStoreTemplateType } from "@/api/objectStores";
|
||||
import { useObjectStoreInstancesStore } from "@/stores/objectStoreInstancesStore";
|
||||
|
||||
import { setupTestPinia } from "./testUtils";
|
||||
|
||||
const type = "s3" as "s3" | "azure_blob" | "disk" | "generic_s3";
|
||||
const type = "aws_s3" as ObjectStoreTemplateType;
|
||||
const TEST_INSTANCE = {
|
||||
type: type,
|
||||
name: "moo",
|
||||
@@ -16,6 +17,9 @@ const TEST_INSTANCE = {
|
||||
private: false,
|
||||
id: 4,
|
||||
uuid: "112f889f-72d7-4619-a8e8-510a8c685aa7",
|
||||
active: true,
|
||||
hidden: false,
|
||||
purged: false,
|
||||
};
|
||||
|
||||
describe("Object Store Instances Store", () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type ObjectStoreTemplateType } from "@/api/objectStores";
|
||||
import { useObjectStoreTemplatesStore } from "@/stores/objectStoreTemplatesStore";
|
||||
|
||||
import { setupTestPinia } from "./testUtils";
|
||||
|
||||
const s3 = "s3" as "s3" | "azure_blob" | "disk" | "generic_s3";
|
||||
const s3 = "aws_s3" as ObjectStoreTemplateType;
|
||||
const TEMPLATES_BASIC = [
|
||||
{
|
||||
type: s3,
|
||||
|
||||
@@ -4,58 +4,13 @@ import { fetcher } from "@/api/schema/fetcher";
|
||||
import type { components } from "@/api/schema/schema";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import { canUpgrade, findTemplate, getLatestVersion, getLatestVersionMap } from "./configTemplatesUtil";
|
||||
|
||||
const getObjectStoreTemplates = fetcher.path("/api/object_store_templates").method("get").create();
|
||||
|
||||
type ObjectStoreTemplateSummary = components["schemas"]["ObjectStoreTemplateSummary"];
|
||||
type ObjectStoreTemplateSummaries = ObjectStoreTemplateSummary[];
|
||||
|
||||
function findTemplate(templates: ObjectStoreTemplateSummaries, templateId: string, templateVersion: number) {
|
||||
for (const template of templates) {
|
||||
if (template.id == templateId && template.version == templateVersion) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLatestVersionMap(templates: ObjectStoreTemplateSummaries): { [key: string]: number } {
|
||||
const latestVersions: { [key: string]: number } = {};
|
||||
templates.forEach((i: ObjectStoreTemplateSummary) => {
|
||||
const templateId = i.id;
|
||||
const templateVersion = i.version || 0;
|
||||
if ((latestVersions[templateId] ?? -1) < templateVersion) {
|
||||
latestVersions[templateId] = templateVersion;
|
||||
}
|
||||
});
|
||||
return latestVersions;
|
||||
}
|
||||
|
||||
function canUpgrade(templates: ObjectStoreTemplateSummaries, templateId: string, templateVersion: number): boolean {
|
||||
let can = false;
|
||||
templates.forEach((i: ObjectStoreTemplateSummary) => {
|
||||
if (i.id == templateId && i.version && i.version > templateVersion) {
|
||||
can = true;
|
||||
}
|
||||
});
|
||||
return can;
|
||||
}
|
||||
|
||||
function getLatestVersion(templates: ObjectStoreTemplateSummaries, id: string): ObjectStoreTemplateSummary | null {
|
||||
let latestVersion = -1;
|
||||
let latestTemplate = null as ObjectStoreTemplateSummary | null;
|
||||
templates.forEach((i: ObjectStoreTemplateSummary) => {
|
||||
const templateId = i.id;
|
||||
if (templateId == id) {
|
||||
const templateVersion = i.version || 0;
|
||||
if (templateVersion > latestVersion) {
|
||||
latestTemplate = i;
|
||||
latestVersion = templateVersion;
|
||||
}
|
||||
}
|
||||
});
|
||||
return latestTemplate;
|
||||
}
|
||||
|
||||
export const useObjectStoreTemplatesStore = defineStore("objectStoreTemplatesStore", {
|
||||
state: () => ({
|
||||
templates: [] as ObjectStoreTemplateSummaries,
|
||||
|
||||
@@ -94,7 +94,8 @@ preferences:
|
||||
email_input: "input[id='email']"
|
||||
username_input: "input[id='username']"
|
||||
preferred_storage: '.preferred-storage'
|
||||
manage_object_stores: '.manage-object-stores'
|
||||
manage_object_stores: '#manage-object-stores'
|
||||
manage_file_sources: '#manage-file-sources'
|
||||
|
||||
object_store_selection:
|
||||
selectors:
|
||||
@@ -104,8 +105,26 @@ preferences:
|
||||
object_store_instances:
|
||||
index:
|
||||
selectors:
|
||||
create_button: object-store-create
|
||||
create_button: '#object-store-create'
|
||||
_: '#user-object-stores-index'
|
||||
|
||||
create:
|
||||
selectors:
|
||||
select: '.object-store-template-select-button[data-template-id="${template_id}"]'
|
||||
_: '#create-object-store-landing'
|
||||
submit: '#submit'
|
||||
|
||||
file_source_instances:
|
||||
index:
|
||||
selectors:
|
||||
create_button: '#file-source-create'
|
||||
_: '#user-file-sources-index'
|
||||
|
||||
create:
|
||||
selectors:
|
||||
select: '.file-source-template-select-button[data-template-id="${template_id}"]'
|
||||
_: '#create-file-source-landing'
|
||||
submit: '#submit'
|
||||
|
||||
toolbox_filters:
|
||||
selectors:
|
||||
|
||||
@@ -7,6 +7,7 @@ export const URI_PREFIXES = [
|
||||
"gxfiles://",
|
||||
"gximport://",
|
||||
"gxuserimport://",
|
||||
"gxuserfiles://",
|
||||
"gxftp://",
|
||||
"drs://",
|
||||
"invenio://",
|
||||
|
||||
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,595 @@
|
||||
# Connecting Users and Data
|
||||
|
||||
Galaxy has countless ways for users to connect with things that might be considered their "data" - file sources (aka "remote files"), object stores (aka "storage locations"), data libraries, the upload API, visualizations, display applications, custom tools, etc...
|
||||
|
||||
This document is going to discuss two of these (file sources and object stores) that are most important Galaxy administrators and how to build Galaxy configuration that allow administrators to let users tie into various pieces of infrastructure.
|
||||
|
||||
```{contents} Table of Contents
|
||||
:depth: 4
|
||||
```
|
||||
|
||||
## Datasets vs Files
|
||||
|
||||
File sources in Galaxy are a sprawling concept but essentially they provide users access to simple files (stored hierarchically into folders) that can be navigated and imported into Galaxy. Importing a "file" into Galaxy generally creates a copy of that file into a Galaxy "object store". Once these files are stored in Galaxy,
|
||||
they become "datasets". A Galaxy dataset is much more than a simple file - Galaxy datasets include various generic metadata a datatype, datatype specific metadata, and ownership and sharing rules managed by Galaxy.
|
||||
|
||||
Galaxy object stores (called "storage locations" in the UI) store datasets and global (accessible to all users) object stores are configured with the ``galaxy.yml`` property ``object_store_config_file`` (or ``object_store_config`` for a configuration embedded right in ``galaxy.yml``) that defaults to ``object_store_conf.xml`` or ``object_store_conf.yml`` if either is present in Galaxy's configuration directory. Galaxy file sources provide users access to raw files and global files sources are configured with the ``galaxy.yml`` property ``file_sources_config_file`` (or ``file_sources`` for embedded configurations) that defaults to ``file_sources_conf.yml`` if that file is present in Galaxy's configuration directory.
|
||||
|
||||
Some of Galaxy's most updated and complete administrator documentation can be found in configuration sample files - this is definitely the case for object stores and file sources. The relevant sample configuration files include [file_sources_conf.yml.sample](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/config/sample/file_sources_conf.yml.sample) and [object_store_conf.sample.yml](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/config/sample/object_store_conf.sample.yml).
|
||||
|
||||
File sources and object stores configured with the above files essentially are available to all users of your Galaxy instance - hence this document describes them as "global" file sources and object stores. File source configurations do allow some templating that does allow the a global file source to be materialized differently for different users. For instance, you as an admin may setup a Dropbox file source and may explicitly add custom user properties that allow that single Dropbox file source to read from a user's preferences. Since there is just one Dropbox service and most people only have a single Dropbox account, this use case can be somewhat adequately addressed by the global file source and the global user preferences file. For a use case like Amazon S3 buckets though for instance, a single bucket file source that is parameterized one way is probably more clearly inadequate. For instance, users would very likely want to attach different buckets for different projects. Additionally, the Galaxy user interface doesn't tie the user preferences to the particular file source and so this method introduces a huge education burden on your Galaxy instance. Finally, the templating available to file sources are not available for object stores - and allowing users to describe how they would like datasets stored and to pay for their own dataset storage are important use cases.
|
||||
|
||||
This document is going to describe Galaxy configuration template libraries that allow the
|
||||
administrator to setup templates for file sources and object stores that your users may instantiate
|
||||
as they see fit. User's can instantiate multiple instances of any template, the template concept
|
||||
can apply to both file source and object store plugins, and the user interface is unified from the
|
||||
template configuration file (you as the admin do not need to explicitly declare user preferences and
|
||||
your users do not need to navigate seemingly unrelated preferences to get plugins to work).
|
||||
|
||||
## Object Store Templates
|
||||
|
||||
Galaxy's object store templates are configured as a YAML list of template objects. This list
|
||||
can be placed ``object_store_templates.yml`` in Galaxy configuration directory (or any path
|
||||
pointed to by the configuration option ``object_store_templates_config_file`` in ``galaxy.yml``).
|
||||
Alternatively, the configuration can be placed directly into ``galaxy.yml`` using the
|
||||
``object_store_templates`` configuration option.
|
||||
|
||||
A minimal object store template might look something like:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/simple_example.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
### Object Store Types
|
||||
|
||||
#### ``disk``
|
||||
|
||||
This is the most basic sort of object store template that just makes disk paths available to users
|
||||
for storing data. Paths can be built up from the user supplied variables, user details, supplied
|
||||
environment variables, etc.. The simple example used to demonstrate these concepts just uses
|
||||
a user supplied project name and the user's username to produce a unique path for each user
|
||||
defined object store.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/simple_example.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
These sorts of object stores have no quota so be careful.
|
||||
|
||||
The syntax for the ``configuration`` section of ``disk`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's object store infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``object_store_conf.yml`` (Galaxy's global object
|
||||
store configuration).
|
||||
|
||||

|
||||
|
||||
#### ``boto3``
|
||||
|
||||
Object stores of the type ``boto3`` can be used to access a wide variety of S3
|
||||
compatible storage services including AWS S3. How you template them can result in widely
|
||||
different experiences for your users and can result in addressing a wide variety of use cases.
|
||||
|
||||
Here is an example that is tailored for a specific storage service (e.g. CloudFlare R2)
|
||||
and exposes just the pieces of data CloudFlare users would need.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/cloudflare.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
Templates can be much more generic or much less generic than this.
|
||||
|
||||
In one direction, all the bells and whistles could be exposed to your Galaxy users to allow
|
||||
them to connect to any S3 compatible storage. This requires a lot more sophistication from
|
||||
your users but also allows them to connect to many more services. This template is available
|
||||
here:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_generic_s3.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
On the other hand, you might run a small lab with a dedicate MinIO storage service and just trust
|
||||
your user's to define individual buckets by name:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/minio_just_buckets.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
If you want to just target AWS S3 and let your users utilize that as quickly
|
||||
and easily as possible that templates might look like this:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_aws_bucket.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
The syntax for the ``configuration`` section of ``boto3`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's object store infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``object_store_conf.yml`` (Galaxy's global object
|
||||
store configuration).
|
||||
|
||||

|
||||
|
||||
#### ``azure_blob``
|
||||
|
||||
Here is a "production grade" Azure template that can be essentially used to connect to any
|
||||
Azure storage container.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_azure_blob.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
This template might be adapted to hide connection details from say users of a individual lab
|
||||
and just expose what container they should use. That might look something like:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/azure_just_container.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
This example is a little contrived though, if a small lab or institution has just a few containers
|
||||
it would likely be a much easier user experience to just wrap them all in a Galaxy hierarchical
|
||||
object store, document them there, and make them available to your whole Galaxy instance.
|
||||
|
||||
The syntax for the ``configuration`` section of ``azure_blob`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's object store infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``object_store_conf.yml`` (Galaxy's global object
|
||||
store configuration).
|
||||
|
||||

|
||||
|
||||
#### ``aws_s3`` (Legacy)
|
||||
|
||||
Object stores of the type ``aws_s3`` are be used to treat AWS Simple Storage Service (S3) buckets
|
||||
as Galaxy object stores. See Amazon documentation for information on [S3](https://aws.amazon.com/s3/)
|
||||
and [how to create buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html)
|
||||
and [how to create access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html).
|
||||
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_aws_s3_legacy.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
The ``aws_s3`` object store is older and more well tested than the ``boto3`` object store, but
|
||||
the ``boto3`` object store is built using a newer, more robust, and more feature-rich client
|
||||
library so it should probably be the object store you use instead of this.
|
||||
|
||||
The syntax for the ``configuration`` section of ``aws_s3`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's object store infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``object_store_conf.yml`` (Galaxy's global object
|
||||
store configuration).
|
||||
|
||||

|
||||
|
||||
#### ``generic_s3`` (Legacy)
|
||||
|
||||
Object stores of the type ``generic_s3`` can be used to access a wide variety of S3
|
||||
compatible storage services. How you template them can result in widely different
|
||||
experiences for your users and can result in addressing a wide variety of use cases.
|
||||
|
||||
Here is an example that is tailored for a specific storage service (e.g. CloudFlare R2)
|
||||
and exposes just the pieces of data CloudFlare users would need.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/cloudflare_legacy.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
Templates can be much more generic or much less generic than this.
|
||||
|
||||
In one direction, all the bells and whistles could be exposed to your Galaxy users to allow
|
||||
them to connect to any S3 compatible storage. This requires a lot more sophistication from
|
||||
your users but also allows them to connect to many more services. This template is available
|
||||
here:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_generic_s3_legacy.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
On the other hand, you might run a small lab with a dedicate MinIO storage service and just trust
|
||||
your user's to define individual buckets by name:
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/minio_just_buckets_legacy.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
The syntax for the ``configuration`` section of ``generic_s3`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's object store infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``object_store_conf.yml`` (Galaxy's global object
|
||||
store configuration).
|
||||
|
||||

|
||||
|
||||
### YAML Syntax
|
||||
|
||||

|
||||
|
||||
### Ready To Use Production Object Store Templates
|
||||
|
||||
The templates have been tested by a Galaxy developer and are sufficiently generic that
|
||||
they may make sense for a variety of Galaxy instances, address a variety of potential use
|
||||
cases, and do not have need any additional tailoring, parameterization, or other
|
||||
customization. These assume your Galaxy instance has a Vault configured and you're
|
||||
comfortable with it storing your user's secrets.
|
||||
|
||||
#### Allow Users to Define Azure Blob Storage as Object Stores
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_azure_blob.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### Allow Users to Define Generic S3 Compatible Storage Services as Object Stores
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_generic_s3.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
#### Allow Users to Define AWS S3 Buckets as Object Stores
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_aws_s3.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
#### Allow Users to Define Google Cloud Provider S3 Interop Storage Buckets as Object Stores
|
||||
|
||||
This template includes descriptions of how to generate HMAC keys used by this interoperability
|
||||
layer provided by Google and lots of links to relevant Google Cloud Storage documentation.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/production_gcp_s3.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
## File Source Templates
|
||||
|
||||
Galaxy's file source templates are configured as a YAML list of template objects. This list
|
||||
can be placed ``file_source_templates.yml`` in Galaxy configuration directory (or any path
|
||||
pointed to by the configuration option ``file_source_templates_config_file`` in ``galaxy.yml``).
|
||||
Alternatively, the configuration can be placed directly into ``galaxy.yml`` using the
|
||||
``file_source_templates`` configuration option.
|
||||
|
||||
|
||||
### File Source Types
|
||||
|
||||
#### ``posix``
|
||||
|
||||
The syntax for the ``configuration`` section of ``posix`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's file source plugin infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``file_sources_conf.yml`` (Galaxy's global file source
|
||||
configuration).
|
||||
|
||||

|
||||
|
||||
#### ``s3fs``
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_s3fs.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_aws_public_bucket.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_aws_private_bucket.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's file source plugin infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``file_sources_conf.yml`` (Galaxy's global file source
|
||||
configuration).
|
||||
|
||||

|
||||
|
||||
#### ``ftp``
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_ftp.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
The syntax for the ``configuration`` section of ``ftp`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's file source plugin infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``file_sources_conf.yml`` (Galaxy's global file source
|
||||
configuration).
|
||||
|
||||

|
||||
|
||||
#### ``azure``
|
||||
|
||||
The syntax for the ``configuration`` section of ``azure`` templates looks like this.
|
||||
|
||||

|
||||
|
||||
At runtime, after the ``configuration`` template is expanded, the resulting dictionary
|
||||
passed to Galaxy's file source plugin infrastructure looks like this and should match a subset
|
||||
of what you'd be able to add directly to ``file_sources_conf.yml`` (Galaxy's global file source
|
||||
configuration).
|
||||
|
||||

|
||||
|
||||
### YAML Syntax
|
||||
|
||||

|
||||
|
||||
### Ready To Use Production File Source Templates
|
||||
|
||||
The templates have been tested by a Galaxy developer and are sufficiently generic that
|
||||
they may make sense for a variety of Galaxy instances, address a variety of potential use
|
||||
cases, and do not have need any additional tailoring, parameterization, or other
|
||||
customization. These assume your Galaxy instance has a Vault configured and you're
|
||||
comfortable with it storing your user's secrets.
|
||||
|
||||
#### Allow Users to Define Generic FTP Servers as File Sources
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_ftp.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### Allow Users to Define Azure Blob Storage as File Sources
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_azure.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### Allow Users to Define Generic S3 Compatible Storage as File Sources
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_s3fs.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
#### Allow Users to Define Publicly Accessible AWS S3 Buckets as File Sources
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_aws_public_bucket.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### Allow Users to Define Private AWS S3 Buckets as File Sources
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_aws_private_bucket.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
## Playing Nicer with Ansible
|
||||
|
||||
Many large instances of Galaxy are configured with Ansible and much of the existing administrator
|
||||
documentation leverages Ansible. The configuration template files using Jinja templating and so
|
||||
does Ansible by default. This might result in a lack of clarity of when templates (strings
|
||||
starting with ``{{`` and ending with ``}}``) are being evaluated. Ansible templates are evaluated
|
||||
at deploy time and the configuration objects describing plugins are evaluated at Galaxy runtime.
|
||||
|
||||
The easiest way to fix this is probably to store these templates files in your Ansible as plain files
|
||||
and not templates. If you'd like to use Ansible templating to build up these files you'll very
|
||||
likely need to tell either Galaxy or Ansible to use something other than ``{{`` and ``}}`` for
|
||||
templating variables. This can be done by placing a directive at the top of your template that
|
||||
is consumed by Ansible. For instance, to have ``[%`` and ``%]`` used instead of ``{{`` and ``}}``
|
||||
by Ansible at deploy time, the file could start with:
|
||||
|
||||
```
|
||||
#jinja2:variable_start_string:'[%' , variable_end_string:'%]'
|
||||
```
|
||||
|
||||
In this case, variables wrapped by ``[%`` and ``%]`` are expanded by Ansible and use the Ansible
|
||||
environment and ``{`` and ``}`` are reserved for Galaxy templating.
|
||||
|
||||
Alternatively, Galaxy can be configured to use a custom template on a per-configuration
|
||||
object basis by setting the ``template_start`` and/or ``template_end`` variables.
|
||||
|
||||
The following template chunk shows how to override the templating Galaxy does for a
|
||||
particular object store configuration. Similar templating overrides work for file source
|
||||
plugin templates.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/templating_override.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
- https://github.com/ansible/ansible/pull/75306
|
||||
- https://stackoverflow.com/questions/12083319/add-custom-tokens-in-jinja2-e-g-somevar
|
||||
|
||||
## Jinja Template Reference
|
||||
|
||||
Galaxy configuration file templating uses [Jinja](https://jinja.palletsprojects.com/en/3.0.x/templates/) to template values and connect inputs, configuration, and the runtime environment
|
||||
into concrete configuration YAML blocks.
|
||||
|
||||
Jinja is fairly straight forward to learn but this document provides tons of examples and
|
||||
one can probably adapt them to whatever you're interested in building without really needing
|
||||
to dig deeply into Jinja. However, this section does outline what Galaxy does inject into the
|
||||
Jinja environment to serve as a reference.
|
||||
|
||||
Even the most exotic configurations will likely only scratch the surface of what Jinja
|
||||
allows and implements. The only relevant Jinja documentation you'll need in these cases
|
||||
is probably just those documents on [variables](https://jinja.palletsprojects.com/en/3.0.x/templates/#variables), [filters](https://jinja.palletsprojects.com/en/3.0.x/templates/#filters),
|
||||
and the [list of builtin filters](https://jinja.palletsprojects.com/en/3.0.x/templates/#list-of-builtin-filters).
|
||||
|
||||
### ``variables``
|
||||
|
||||
This is a typed dictionary object is populated with user supplied values defined via the the ``variables`` section of the configuration template and filled in by the user when they
|
||||
created a new object store or file source.
|
||||
|
||||
### ``secrets``
|
||||
|
||||
This is a dictionary of strings populated with user supplied secrets defined via the the ``secrets`` section of the configuration template and filled in by the user when they
|
||||
created a new object store or file source.
|
||||
|
||||
A deep dive into these can be found in the [User Secrets](#user-secrets) section of this document.
|
||||
|
||||
### ``environment``
|
||||
|
||||
This dictionary object is populated with admin-supplied values defined via the the ``environment``
|
||||
section of the configuration template.
|
||||
|
||||
A deep dive into these can be found in the [Admin Secrets](#admin-secrets) section of this document.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/admin_secrets_with_defaults.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
### ``user``
|
||||
|
||||
This dictionary object exposes information about user configuring and using a target template
|
||||
configuration. These values are populated from the ``galaxy_user`` table of the Galaxy database.
|
||||
The current properties exposed include:
|
||||
|
||||
| Key | Description |
|
||||
|--------------|-----------------------------------------------------------|
|
||||
| ``username`` | string corresponding the username of the Galaxy user |
|
||||
| ``email`` | string corresponding the email of the Galaxy user |
|
||||
| ``id`` | integer primary key of user object in the Galaxy database |
|
||||
|
||||
|
||||
The simple example of project scratch storage used to describe these concepts made
|
||||
use the Galaxy user's username to generate unique paths.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/simple_example.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
### ``ensure_path_component``
|
||||
|
||||
This [Jinja filter](https://jinja.palletsprojects.com/en/3.0.x/templates/#filters)
|
||||
will fail template evaluation if the value it is applies to is not
|
||||
a simple directory name. If it contain ``..`` or ``/`` or in some other way might
|
||||
be used to attempt path exploitation of cause odd path-related bugs. This is
|
||||
useful when producing paths for ``disk`` object stores or ``posix`` file sources.
|
||||
|
||||
When taking inputs from users, setting the type of ``path_component`` instead of
|
||||
``string`` allows the client to validate potential issues way before this point,
|
||||
but many path components might be built from environment variables or usernames
|
||||
or sources like this that are not explicitly user inputs.
|
||||
|
||||
An example of an object store template that uses this is the simple scratch example
|
||||
that was used to introduce concepts at the start of the object store template
|
||||
documentation above.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/simple_example.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
### ``asbool``
|
||||
|
||||
This [Jinja filter](https://jinja.palletsprojects.com/en/3.0.x/templates/#filters)
|
||||
will use Galaxy configuration style logic to convert string values into boolean ones.
|
||||
|
||||
When taking inputs from users, setting the type of ``boolean`` is sufficient to ensure
|
||||
a variable is boolean, but "secrets" and environment variables and many other things
|
||||
are likely to be of type string but should be used in a template that expects boolean
|
||||
values.
|
||||
|
||||
An example of an object store template that uses this is ``secure`` environment parameter
|
||||
on the simple minio example.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/objectstore/templates/examples/minio_example.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
|
||||
## Connecting Configuration Templates to Secrets
|
||||
|
||||
(user-secrets)=
|
||||
### User Secrets
|
||||
|
||||
Most of the examples in this document use secrets of one kind or another. For instance, in the FTP
|
||||
example - the password field is a secret.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/production_ftp.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
Instead of being saved in the database in plain text, Galaxy will use a configured Vault to store
|
||||
this data. Check out Galaxy admin documentation on [Storing secrets in the vault](https://docs.galaxyproject.org/en/master/admin/special_topics/vault.html) for descriptions of how to configure
|
||||
a vault. Most interesting user defined file sources and/or object stores will require a Galaxy Vault.
|
||||
|
||||
In this FTP example, a new Vault key will be created for each FTP instance the user creates.
|
||||
The user file source APIs and management user interface will be responsible for orchestration of
|
||||
storing and updating secrets. The Vault key for this password will be something like:
|
||||
|
||||
```
|
||||
/galaxy/user/<user_id>/file_source_config/<file_source_instance_uuid>/password
|
||||
```
|
||||
|
||||
Here ``user_id`` is the primary key of the User object in the database and
|
||||
``file_source_instance_uuid`` is the ``uuid`` value corresponding to the ``user_file_source``
|
||||
table in the database.
|
||||
|
||||
User defined object stores are stored in a similar fashion but at:
|
||||
|
||||
```
|
||||
/galaxy/user/<user_id>/object_store_config/<object_store_instance_uuid>/<secret_name>
|
||||
```
|
||||
|
||||
During the creation of an object store or file source, the secrets will be appended to the generated
|
||||
form as password fields.
|
||||
|
||||

|
||||
|
||||
After an object store has been created, a user has the option to edit the settings in the UI.
|
||||
Most of the settings appear in a simple form - but the secrets are managed and updated
|
||||
individually in the "Secrets" tab.
|
||||
|
||||

|
||||
|
||||
|
||||
(admin-secrets)=
|
||||
### Admin Secrets
|
||||
|
||||
Administrators may define secrets that are available to all users and aren't parameterized
|
||||
on a per-instance basis. These secrets can be injected into template instances through Vault
|
||||
keys or through environment variables.
|
||||
|
||||
Each template may optionally define an ``environment`` key where these can be defined. The
|
||||
following template entry describes a file source that injects the environment variable
|
||||
``GALAXY_SECRET_HOME_VAR`` into the template as ``environment.var`` and injects the Vault
|
||||
key ``secret_directory_file_source/my_secret`` into the template as ``environment.var``.
|
||||
This template uses these variables to construct a root path for a ``posix`` file source
|
||||
but the same secrets could just as easily store cloud keys and configure an S3 object store.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/admin_secrets.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
If you'd like to make the target secrets optional, default values can also be setup.
|
||||
The following block demonstrates the same configuration but with default values of
|
||||
``default_var`` for the default ``var`` value and ``default_sec`` for the default ``sec``
|
||||
value. These will be used in the target Vault keys are absent or the target environment
|
||||
variable is not defined at runtime.
|
||||
|
||||
```{literalinclude} ../../../lib/galaxy/files/templates/examples/admin_secrets_with_defaults.yml
|
||||
:language: yaml
|
||||
```
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 316 KiB |
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import erdantic as erd
|
||||
|
||||
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "lib")))
|
||||
|
||||
from galaxy.files.templates.models import (
|
||||
AzureFileSourceConfiguration,
|
||||
AzureFileSourceTemplateConfiguration,
|
||||
FileSourceTemplate,
|
||||
FtpFileSourceConfiguration,
|
||||
FtpFileSourceTemplateConfiguration,
|
||||
PosixFileSourceConfiguration,
|
||||
PosixFileSourceTemplateConfiguration,
|
||||
S3FSFileSourceConfiguration,
|
||||
S3FSFileSourceTemplateConfiguration,
|
||||
)
|
||||
from galaxy.objectstore.templates.models import (
|
||||
AwsS3ObjectStoreConfiguration,
|
||||
AwsS3ObjectStoreTemplateConfiguration,
|
||||
AzureObjectStoreConfiguration,
|
||||
AzureObjectStoreTemplateConfiguration,
|
||||
Boto3ObjectStoreConfiguration,
|
||||
Boto3ObjectStoreTemplateConfiguration,
|
||||
DiskObjectStoreConfiguration,
|
||||
DiskObjectStoreTemplateConfiguration,
|
||||
GenericS3ObjectStoreConfiguration,
|
||||
GenericS3ObjectStoreTemplateConfiguration,
|
||||
ObjectStoreTemplate,
|
||||
)
|
||||
|
||||
DOC_SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__)))
|
||||
|
||||
class_to_diagram = {
|
||||
ObjectStoreTemplate: "object_store_templates",
|
||||
AzureObjectStoreConfiguration: "object_store_azure_configuration",
|
||||
AzureObjectStoreTemplateConfiguration: "object_store_azure_configuration_template",
|
||||
Boto3ObjectStoreConfiguration: "object_store_boto3_configuration",
|
||||
Boto3ObjectStoreTemplateConfiguration: "object_store_boto3_configuration_template",
|
||||
DiskObjectStoreConfiguration: "object_store_disk_configuration",
|
||||
DiskObjectStoreTemplateConfiguration: "object_store_disk_configuration_template",
|
||||
AwsS3ObjectStoreTemplateConfiguration: "object_store_aws_s3_configuration_template",
|
||||
AwsS3ObjectStoreConfiguration: "object_store_aws_s3_configuration",
|
||||
GenericS3ObjectStoreTemplateConfiguration: "object_store_generic_s3_configuration_template",
|
||||
GenericS3ObjectStoreConfiguration: "object_store_generic_s3_configuration",
|
||||
FileSourceTemplate: "file_source_templates",
|
||||
AzureFileSourceTemplateConfiguration: "file_source_azure_configuration_template",
|
||||
AzureFileSourceConfiguration: "file_source_azure_configuration",
|
||||
PosixFileSourceTemplateConfiguration: "file_source_posix_configuration_template",
|
||||
PosixFileSourceConfiguration: "file_source_posix_configuration",
|
||||
S3FSFileSourceTemplateConfiguration: "file_source_s3fs_configuration_template",
|
||||
S3FSFileSourceConfiguration: "file_source_s3fs_configuration",
|
||||
FtpFileSourceTemplateConfiguration: "file_source_ftp_configuration_template",
|
||||
FtpFileSourceConfiguration: "file_source_ftp_configuration",
|
||||
}
|
||||
|
||||
for clazz, diagram_name in class_to_diagram.items():
|
||||
erd.draw(clazz, out=f"{DOC_SOURCE_DIR}/{diagram_name}.png")
|
||||
@@ -11,6 +11,7 @@ This documentation is in the midst of being ported and unified based on resource
|
||||
config
|
||||
config_logging
|
||||
production
|
||||
data
|
||||
security
|
||||
nginx
|
||||
apache
|
||||
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 693 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,39 @@
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
THIS_DIRECTORY = os.path.dirname(__file__)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="search_for_new_screenshots",
|
||||
description="Searches a Selenium screenshot directory for screenshots used in the admin docs and grabs new ones if available",
|
||||
)
|
||||
|
||||
SEARCH_FOR = [
|
||||
"user_object_store_form_empty_azure.png",
|
||||
"user_object_store_form_full_azure.png",
|
||||
"user_object_store_form_empty_generic_s3.png",
|
||||
"user_object_store_form_full_generic_s3.png",
|
||||
"user_object_store_form_empty_aws_s3.png",
|
||||
"user_object_store_form_full_aws_s3.png",
|
||||
"user_object_store_form_empty_gcp_s3_interop.png",
|
||||
"user_object_store_form_full_gcp_s3_interop.png",
|
||||
"user_file_source_form_full_aws_public.png",
|
||||
"user_file_source_form_full_azure.png",
|
||||
"user_file_source_form_full_ftp.png",
|
||||
]
|
||||
|
||||
|
||||
def main(argv):
|
||||
parser.add_argument("screenshot_directory")
|
||||
args = parser.parse_args(argv[1:])
|
||||
screenshot_directory = args.screenshot_directory
|
||||
for filename in os.listdir(screenshot_directory):
|
||||
if filename in SEARCH_FOR:
|
||||
print(f"Found useful screenshot {filename}, copying to {THIS_DIRECTORY}")
|
||||
shutil.copy(os.path.join(screenshot_directory, filename), os.path.join(THIS_DIRECTORY, filename))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
|
After Width: | Height: | Size: 386 KiB |