Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import { useStore } from 'vuex';

import { EPINIO_TYPES } from '../../types';
Expand Down Expand Up @@ -38,6 +38,10 @@ const isEdit = computed(() => modalMode.value === 'edit');
const isCreate = computed(() => modalMode.value === 'create');
const isEditing = computed(() => isEdit.value || isCreate.value);

const isLoadingApplications = ref(false);
const cachedApplications = ref<any[]>([]);
const fetchedApplications = ref<any[]>([]);

const {
options: namespaceOpts,
isLoading: isLoadingNamespaces,
Expand All @@ -48,12 +52,10 @@ const {
scopeToActiveFilter: true
});

// Filter apps to those in the selected namespace, and map to dropdown options
const nsAppOptions = computed(() => {
if (!formNamespace.value) return [];

return store.getters['epinio/all'](EPINIO_TYPES.APP)
.filter((a: any) => a.meta.namespace === formNamespace.value)
return fetchedApplications.value
.map((a: any) => ({ label: a.meta.name, value: a.meta.name }));
});

Expand Down Expand Up @@ -388,6 +390,69 @@ async function onSubmit() {
}
}

async function fetchApplications() {
if (!formNamespace.value) return;

if (cachedApplications.value.length > 0) {
fetchedApplications.value = cachedApplications.value;
return;
}

isLoadingApplications.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/applications?namespaces=${formNamespace.value}`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.APP, ...item })
));
fetchedApplications.value = classifiedData;
cachedApplications.value = classifiedData;
} catch (error) {
console.error('Failed to fetch applications', error);
} finally {
isLoadingApplications.value = false;
}
}

async function searchApplications(query: string) {
if (!formNamespace.value) return;

isLoadingApplications.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/applications?namespaces=${formNamespace.value}&search=${query}`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.APP, ...item })
));
fetchedApplications.value = classifiedData;
} catch {
fetchedApplications.value = [];
} finally {
isLoadingApplications.value = false;
}
}

// watch namespace changes to fetch applications for the selected namespace
watch(formNamespace, (newNamespace) => {
if (newNamespace) {
fetchedApplications.value = [];
cachedApplications.value = [];
fetchApplications();
}
}, { immediate: true });

defineExpose({ openCreate, openView, openEdit });
</script>

Expand Down Expand Up @@ -456,16 +521,18 @@ defineExpose({ openCreate, openView, openEdit });

<!-- Bind to Application -->
<trailhand-form-row>
<trailhand-dropdown
style="width: 100%"
:options="nsAppOptions"
<ResourceDropdown
:values="selectedApps"
:options="nsAppOptions"
label="Bind to Application (Optional)"
:disabled="isView"
:multiselect="true"
:filterable="true"
filterable
multiselect
placeholder="Select applications to bind"
@dropdown-change="(e: CustomEvent) => { selectedApps = e.detail.values; }"
:onDropdownChange="(e: CustomEvent) => { selectedApps = e.detail.values; }"
:fetchAllResources="fetchApplications"
:searchResources="searchApplications"
:isLoading="isLoadingApplications"
/>
</trailhand-form-row>

Expand Down
178 changes: 149 additions & 29 deletions dashboard/pkg/epinio/components/service/ServiceInstanceModal.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, reactive } from 'vue';
import { computed, ref, reactive, watch } from 'vue';
import { useStore } from 'vuex';

import { EPINIO_TYPES } from '../../types';
Expand Down Expand Up @@ -37,6 +37,14 @@ const errors = ref<string[]>([]);
// Captured separately so background list polls (which omit internal_routes) can't wipe it
const internalRoutes = ref<string[]>([]);

const isLoadingCatalogServices = ref(false);
const cachedCatalogServices = ref<any[]>([]);
const fetchedCatalogServices = ref<any[]>([]);

const isLoadingApplications = ref(false);
const cachedApplications = ref<any[]>([]);
const fetchedApplications = ref<any[]>([]);

const {
options: namespaceOpts,
isLoading: isLoadingNamespaces,
Expand All @@ -48,12 +56,8 @@ const {
scopeToActiveFilter: true
});

const catalogServices = computed(() =>
store.getters['epinio/all'](EPINIO_TYPES.CATALOG_SERVICE)
);

const catalogServiceOpts = computed(() =>
catalogServices.value.map((cs: any) => ({
fetchedCatalogServices.value.map((cs: any) => ({
label: `${cs.name} (${cs.short_description})`,
value: cs.name,
}))
Expand All @@ -62,17 +66,16 @@ const catalogServiceOpts = computed(() =>
const nsAppOptions = computed(() => {
if (!formNamespace.value) return [];

return store.getters['epinio/all'](EPINIO_TYPES.APP)
.filter((a: any) => a.meta.namespace === formNamespace.value)
return fetchedApplications.value
.map((a: any) => ({ label: a.meta.name, value: a.meta.name }));
});

const selectedCatalogService = computed(() =>
catalogServices.value.find((cs: any) => cs.name === formCatalogService.value)
fetchedCatalogServices.value.find((cs: any) => cs.name === formCatalogService.value)
);

const showChartValues = computed(() =>
Object.keys(selectedCatalogService.value?.settings || {}).length !== 0
Object.keys(selectedCatalogService.value?.settings || {}).length !== 0 || (isEdit.value && Object.keys(chartValues).length !== 0)
);

const isEdit = computed(() => modalMode.value === 'edit');
Expand Down Expand Up @@ -375,6 +378,120 @@ async function onSubmit() {
}
}

const fetchCatalogServices = async () => {
if (cachedCatalogServices.value.length > 0) {
fetchedCatalogServices.value = cachedCatalogServices.value;
return;
}
isLoadingCatalogServices.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/catalogservices`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];

// classify raw JSON into proper model instances
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.CATALOG_SERVICE, ...item })
));
fetchedCatalogServices.value = classifiedData;
cachedCatalogServices.value = classifiedData;
} catch (error) {
console.error('Failed to fetch services', error);
} finally {
isLoadingCatalogServices.value = false;
}
};

async function searchCatalogServices(query: string) {
isLoadingCatalogServices.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/catalogservices?search=${query}`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.CATALOG_SERVICE, ...item })
));
fetchedCatalogServices.value = classifiedData;
} catch {
fetchedCatalogServices.value = [];
} finally {
isLoadingCatalogServices.value = false;
}
}

async function fetchApplications() {
if (!formNamespace.value) return;

if (cachedApplications.value.length > 0) {
fetchedApplications.value = cachedApplications.value;
return;
}

isLoadingApplications.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/applications?namespaces=${formNamespace.value}`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.APP, ...item })
));
fetchedApplications.value = classifiedData;
cachedApplications.value = classifiedData;
} catch (error) {
console.error('Failed to fetch applications', error);
} finally {
isLoadingApplications.value = false;
}
}

async function searchApplications(query: string) {
if (!formNamespace.value) return;

isLoadingApplications.value = true;
try {
const res = await store.dispatch('epinio/request', {
opt: {
url: `/api/v1/applications?namespaces=${formNamespace.value}&search=${query}`,
method: 'GET',
responseType: 'json'
}
});
const rawData = res.data ?? [];
const classifiedData = await Promise.all(rawData.map((item: any) =>
store.dispatch('epinio/create', { type: EPINIO_TYPES.APP, ...item })
));
fetchedApplications.value = classifiedData;
} catch {
fetchedApplications.value = [];
} finally {
isLoadingApplications.value = false;
}
}

// watch namespace changes to fetch applications for the selected namespace
watch(formNamespace, (newNamespace) => {
if (newNamespace) {
fetchedApplications.value = [];
cachedApplications.value = [];
fetchApplications();
}
}, { immediate: true });

defineExpose({ openCreate, openEdit, openView });
</script>

Expand Down Expand Up @@ -414,17 +531,18 @@ defineExpose({ openCreate, openEdit, openView });

<!-- Catalog Service + Version (version only in view/edit, 3/4 + 1/4 split via 4-col grid) -->
<trailhand-form-row :columns="(isView || isEdit) ? '4' : '1'">
<trailhand-dropdown
:style="(isView || isEdit) ? 'width: 100%; grid-column: span 3' : 'width: 100%'"
:options="catalogServiceOpts"
<ResourceDropdown
:value="formCatalogService"
label="Catalog Service"
:required="!isView"
:options="catalogServiceOpts"
:label="'Catalog Service'"
:disabled="isEdit || isView"
:filterable="true"
filterable
placeholder="Select the type of service to create"
@dropdown-change="(e: CustomEvent) => { formCatalogService = e.detail.value; resetChartValues(); }"
></trailhand-dropdown>
:onDropdownChange="(e: CustomEvent) => { formCatalogService = e.detail.value; resetChartValues(); }"
:fetchAllResources="fetchCatalogServices"
:searchResources="searchCatalogServices"
:isLoading="isLoadingCatalogServices"
/>
<trailhand-text-input
v-if="isView || isEdit"
:value="serviceModel?.catalog_service_version || ''"
Expand All @@ -445,17 +563,19 @@ defineExpose({ openCreate, openEdit, openView });

<!-- Bind to Application -->
<trailhand-form-row>
<trailhand-dropdown
style="width: 100%"
:options="nsAppOptions"
:values="selectedApps"
label="Bind to Application (Optional)"
:disabled="isView"
:multiselect="true"
:filterable="true"
placeholder="Select applications to bind"
@dropdown-change="(e: CustomEvent) => { selectedApps = e.detail.values; }"
></trailhand-dropdown>
<ResourceDropdown
:values="selectedApps"
:options="nsAppOptions"
label="Bind to Application (Optional)"
:disabled="isView"
filterable
multiselect
placeholder="Select applications to bind"
:onDropdownChange="(e: CustomEvent) => { selectedApps = e.detail.values; }"
:fetchAllResources="fetchApplications"
:searchResources="searchApplications"
:isLoading="isLoadingApplications"
/>
</trailhand-form-row>

<!-- Chart Values (shown when the selected catalog service has configurable settings) -->
Expand Down
Loading
Loading