diff --git a/ui/src/assets/demo/assets.png b/ui/src/assets/demo/assets.png new file mode 100644 index 0000000000..95a684d501 Binary files /dev/null and b/ui/src/assets/demo/assets.png differ diff --git a/ui/src/components/demo/Assets.vue b/ui/src/components/demo/Assets.vue new file mode 100644 index 0000000000..0a48b797f9 --- /dev/null +++ b/ui/src/components/demo/Assets.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/ui/src/components/demo/Layout.vue b/ui/src/components/demo/Layout.vue index ea94cf83c5..1c611f37b4 100644 --- a/ui/src/components/demo/Layout.vue +++ b/ui/src/components/demo/Layout.vue @@ -105,7 +105,7 @@ position: relative; background: $base-gray-200; padding: .125rem 0.5rem; - border-radius: $border-radius; + border-radius: 1rem; display: inline-block; z-index: 2; margin: 0 auto; @@ -175,6 +175,7 @@ line-height: 16px; font-size: 11px; text-align: left; + color: var(--ks-content-secondary); } .video-container { @@ -261,7 +262,7 @@ } p { - font-size: 14px; + font-size: 1rem; line-height: 22px; } } diff --git a/ui/src/components/dependencies/Dependencies.vue b/ui/src/components/dependencies/Dependencies.vue index dc15bf6afa..77d3de94b2 100644 --- a/ui/src/components/dependencies/Dependencies.vue +++ b/ui/src/components/dependencies/Dependencies.vue @@ -42,6 +42,7 @@ :elements="getElements()" @select="selectNode" :selected="selectedNodeID" + :subtype="SUBTYPE" /> @@ -54,7 +55,7 @@ import Empty from "../layout/empty/Empty.vue"; import {useDependencies} from "./composables/useDependencies"; - import {FLOW, EXECUTION, NAMESPACE} from "./utils/types"; + import {FLOW, EXECUTION, NAMESPACE, ASSET} from "./utils/types"; const PANEL = {size: "70%", min: "30%", max: "80%"}; @@ -66,13 +67,27 @@ import SelectionRemove from "vue-material-design-icons/SelectionRemove.vue"; import FitToScreenOutline from "vue-material-design-icons/FitToScreenOutline.vue"; - const SUBTYPE = route.name === "flows/update" ? FLOW : route.name === "namespaces/update" ? NAMESPACE : EXECUTION; + const props = defineProps<{ + fetchAssetDependencies?: () => Promise<{ + data: any[]; + count: number; + }>; + }>(); + + const SUBTYPE = route.name === "flows/update" ? FLOW : route.name === "namespaces/update" ? NAMESPACE : route.name === "assets/update" ? ASSET : EXECUTION; const container = ref(null); - const initialNodeID: string = SUBTYPE === FLOW || SUBTYPE === NAMESPACE ? String(route.params.id) : String(route.params.flowId); + const initialNodeID: string = SUBTYPE === FLOW || SUBTYPE === NAMESPACE || SUBTYPE === ASSET ? String(route.params.id || route.params.assetId) : String(route.params.flowId); const TESTING = false; // When true, bypasses API data fetching and uses mock/test data. - const {getElements, isLoading, isRendering, selectedNodeID, selectNode, handlers} = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING); + const { + getElements, + isLoading, + isRendering, + selectedNodeID, + selectNode, + handlers, + } = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING, props.fetchAssetDependencies); diff --git a/ui/src/components/dependencies/composables/useDependencies.ts b/ui/src/components/dependencies/composables/useDependencies.ts index d26f475c47..fda0c579d8 100644 --- a/ui/src/components/dependencies/composables/useDependencies.ts +++ b/ui/src/components/dependencies/composables/useDependencies.ts @@ -163,7 +163,7 @@ const setExecutionEdgeColors = throttle( * @param classes - An array of class names to remove from all elements. * Defaults to [`selected`, `faded`, `hovered`, `executions`]. */ -export function clearClasses(cy: cytoscape.Core, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE, classes: string[] = [SELECTED, FADED, HOVERED, EXECUTIONS]): void { +export function clearClasses(cy: cytoscape.Core, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET, classes: string[] = [SELECTED, FADED, HOVERED, EXECUTIONS]): void { cy.elements().removeClass(classes.join(" ")); if (subtype === EXECUTION) cy.edges().style(edgeColors()); } @@ -197,7 +197,7 @@ export function fit(cy: cytoscape.Core, padding: number = 50): void { * @param subtype - Determines how connected elements are highlighted (`FLOW`, `EXECUTION` or `NAMESPACE`). * @param id - Optional explicit ID to assign to the ref (defaults to the node’s own ID). */ -function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE, id?: Node["id"]): void { +function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET, id?: Node["id"]): void { // Clear all existing classes clearClasses(cy, subtype); @@ -263,7 +263,17 @@ function hoverHandler(cy: cytoscape.Core): void { * @returns An object with element getters, loading state, rendering state, selected node ID, * selection helpers, and control handlers. */ -export function useDependencies(container: Ref, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE = FLOW, initialNodeID: string, params: RouteParams, isTesting = false) { +export function useDependencies( + container: Ref, + subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET = FLOW, + initialNodeID: string, + params: RouteParams, + isTesting = false, + fetchAssetDependencies?: () => Promise<{ + data: Element[]; + count: number; + }> +) { const coreStore = useCoreStore(); const flowStore = useFlowStore(); const executionsStore = useExecutionsStore(); @@ -301,7 +311,13 @@ export function useDependencies(container: Ref, subtype: typ } }; - const elements = ref<{ data: cytoscape.ElementDefinition[]; count: number; }>({data: [], count: 0}); + const elements = ref<{ + data: cytoscape.ElementDefinition[]; + count: number; + }>({ + data: [], + count: 0, + }); onMounted(async () => { if (isTesting) { if (!container.value) { @@ -313,13 +329,32 @@ export function useDependencies(container: Ref, subtype: typ isLoading.value = false; } else { try { - if (subtype === NAMESPACE) { - const {data} = await namespacesStore.loadDependencies({namespace: params.id as string}); + if (fetchAssetDependencies) { + const result = await fetchAssetDependencies(); + elements.value = { + data: result.data, + count: result.count + }; + isLoading.value = false; + } else if (subtype === NAMESPACE) { + const {data} = await namespacesStore.loadDependencies({ + namespace: params.id as string, + }); const nodes = data.nodes ?? []; - elements.value = {data: transformResponse(data, NAMESPACE), count: new Set(nodes.map((r: { uid: string }) => r.uid)).size}; + elements.value = { + data: transformResponse(data, NAMESPACE), + count: new Set(nodes.map((r: { uid: string }) => r.uid)).size, + }; isLoading.value = false; } else { - const result = await flowStore.loadDependencies({id: (subtype === FLOW ? params.id : params.flowId) as string, namespace: params.namespace as string, subtype}, false); + const result = await flowStore.loadDependencies( + { + id: (subtype === FLOW ? params.id : params.flowId) as string, + namespace: params.namespace as string, + subtype, + }, + false + ); elements.value = {data: result.data ?? [], count: result.count}; isLoading.value = false; } @@ -448,8 +483,16 @@ export function useDependencies(container: Ref, subtype: typ selectedNodeID, selectNode, handlers: { - zoomIn: () => cy.zoom({level: cy.zoom() + 0.1, renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition()}), - zoomOut: () => cy.zoom({level: cy.zoom() - 0.1, renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition()}), + zoomIn: () => + cy.zoom({ + level: cy.zoom() + 0.1, + renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition(), + }), + zoomOut: () => + cy.zoom({ + level: cy.zoom() - 0.1, + renderedPosition: cy.getElementById(selectedNodeID.value!).renderedPosition(), + }), clearSelection: () => { clearClasses(cy, subtype); selectedNodeID.value = undefined; @@ -468,9 +511,23 @@ export function useDependencies(container: Ref, subtype: typ * @param subtype - The node subtype, either `FLOW`, `EXECUTION`, or `NAMESPACE`. * @returns An array of cytoscape elements with correctly typed nodes and edges. */ -export function transformResponse(response: {nodes: { uid: string; namespace: string; id: string }[]; edges: { source: string; target: string }[]; }, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE): Element[] { - const nodes: Node[] = response.nodes.map((node) => ({id: node.uid, type: NODE, flow: node.id, namespace: node.namespace, metadata: {subtype}})); - const edges: Edge[] = response.edges.map((edge) => ({id: uuid(), type: EDGE, source: edge.source, target: edge.target})); +export function transformResponse(response: {nodes: { uid: string; namespace: string; id: string }[]; edges: { source: string; target: string }[];}, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE): Element[] { + const nodes: Node[] = response.nodes.map((node) => ({ + id: node.uid, + type: NODE, + flow: node.id, + namespace: node.namespace, + metadata: {subtype}, + })); + const edges: Edge[] = response.edges.map((edge) => ({ + id: uuid(), + type: EDGE, + source: edge.source, + target: edge.target, + })); - return [...nodes.map((node) => ({data: node}) as Element), ...edges.map((edge) => ({data: edge}) as Element)]; + return [ + ...nodes.map((node) => ({data: node}) as Element), + ...edges.map((edge) => ({data: edge}) as Element), + ]; } diff --git a/ui/src/components/dependencies/utils/types.ts b/ui/src/components/dependencies/utils/types.ts index 29c539da90..cfc45edd69 100644 --- a/ui/src/components/dependencies/utils/types.ts +++ b/ui/src/components/dependencies/utils/types.ts @@ -4,6 +4,7 @@ export const EDGE = "EDGE" as const; export const FLOW = "FLOW" as const; export const EXECUTION = "EXECUTION" as const; export const NAMESPACE = "NAMESPACE" as const; +export const ASSET = "ASSET" as const; type Flow = { subtype: typeof FLOW; @@ -19,12 +20,16 @@ type Namespace = { subtype: typeof NAMESPACE; }; +type Asset = { + subtype: typeof ASSET; +}; + export type Node = { id: string; type: "NODE"; flow: string; namespace: string; - metadata: Flow | Execution | Namespace; + metadata: Flow | Execution | Namespace | Asset; }; export type Edge = { diff --git a/ui/src/components/filter/components/layout/FilterChip.vue b/ui/src/components/filter/components/layout/FilterChip.vue index 9492b9105f..f5b57cf1eb 100644 --- a/ui/src/components/filter/components/layout/FilterChip.vue +++ b/ui/src/components/filter/components/layout/FilterChip.vue @@ -152,6 +152,8 @@ font-size: 12px; color: var(--ks-content-primary); white-space: nowrap; + display: flex; + align-items: center; } .value { font-weight: 700; diff --git a/ui/src/components/filter/components/layout/FilterEditPopper.vue b/ui/src/components/filter/components/layout/FilterEditPopper.vue index 90f9393ee4..aaf1f17072 100644 --- a/ui/src/components/filter/components/layout/FilterEditPopper.vue +++ b/ui/src/components/filter/components/layout/FilterEditPopper.vue @@ -86,7 +86,7 @@ ); const isKVPairFilter = computed(() => - props.filterKey?.valueType === "key-value" || (props.filterKey?.key === "labels" && KV_COMPARATORS.includes(state.selectedComparator)) + props.filterKey?.valueType === "key-value" ); const valueComponent = computed(() => { diff --git a/ui/src/components/filter/composables/useFilters.ts b/ui/src/components/filter/composables/useFilters.ts index 5642758491..7d5cb9a635 100644 --- a/ui/src/components/filter/composables/useFilters.ts +++ b/ui/src/components/filter/composables/useFilters.ts @@ -14,7 +14,6 @@ import { COMPARATOR_LABELS, Comparators, TEXT_COMPARATORS, - KV_COMPARATORS } from "../utils/filterTypes"; import {usePreAppliedFilters} from "./usePreAppliedFilters"; import {useDefaultFilter} from "./useDefaultFilter"; @@ -67,11 +66,11 @@ export function useFilters( }; const clearLegacyParams = (query: Record) => { - configuration.keys?.forEach(({key}) => { + configuration.keys?.forEach(({key, valueType}) => { delete query[key]; - if (key === "details") { + if (valueType === "key-value") { Object.keys(query).forEach(queryKey => { - if (queryKey.startsWith("details.")) delete query[queryKey]; + if (queryKey.startsWith(`${key}.`)) delete query[queryKey]; }); } }); @@ -85,10 +84,10 @@ export function useFilters( */ const buildLegacyQuery = (query: Record) => { getUniqueFilters(appliedFilters.value.filter(isValidFilter)).forEach(filter => { - if (filter.key === "details") { + if (configuration.keys?.find(k => k.key === filter.key)?.valueType === "key-value") { (filter.value as string[]).forEach(item => { const [k, v] = item.split(":"); - query[`details.${k}`] = v; + query[`${filter.key}.${k}`] = v; }); } else if (Array.isArray(filter.value)) { filter.value.forEach(item => @@ -108,8 +107,6 @@ export function useFilters( const query = {...route.query}; clearFilterQueryParams(query); - delete query.page; - if (legacyQuery) { clearLegacyParams(query); buildLegacyQuery(query); @@ -119,6 +116,15 @@ export function useFilters( } updateSearchQuery(query); + + if ( + (appliedFilters.value.some(f => Array.isArray(f.value) && f.value.length > 0) + || searchQuery.value.trim()) + && parseInt(String(query.page ?? "1")) > 1 + ) { + delete query.page; + } + router.push({query}); }; @@ -145,14 +151,13 @@ export function useFilters( value: string | string[] ): AppliedFilter => { const comparator = (config?.comparators?.[0] as Comparators) ?? Comparators.EQUALS; - const valueLabel = Array.isArray(value) - ? key === "details" && value.length > 1 - ? `${value[0]} +${value.length - 1}` + return createAppliedFilter(key, config, comparator, value, + config?.valueType === "key-value" && Array.isArray(value) + ? value.length > 1 ? `${value[0]} +${value.length - 1}` : value[0] ?? "" : Array.isArray(value) ? value.join(", ") - : value[0] - : (value as string); - return createAppliedFilter(key, config, comparator, value, valueLabel, "EQUALS"); + : value as string + , "EQUALS"); }; const createTimeRangeFilter = ( @@ -161,14 +166,13 @@ export function useFilters( endDate: Date, comparator = Comparators.EQUALS ): AppliedFilter => { - const valueLabel = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`; return { ...createAppliedFilter( "timeRange", config, comparator, {startDate, endDate}, - valueLabel, + `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`, keyOfComparator(comparator) ), comparatorLabel: "Is Between" @@ -181,34 +185,36 @@ export function useFilters( */ const parseLegacyFilters = (): AppliedFilter[] => { const filtersMap = new Map(); - const details: string[] = []; + const keyValueFilters: Record = {}; Object.entries(route.query).forEach(([key, value]) => { if (["q", "search", "filters[q][EQUALS]"].includes(key)) return; - if (key.startsWith("details.")) { - details.push(`${key.split(".")[1]}:${value}`); + const kvConfig = configuration.keys?.find(k => key.startsWith(`${k.key}.`) && k.valueType === "key-value"); + if (kvConfig) { + if (!keyValueFilters[kvConfig.key]) keyValueFilters[kvConfig.key] = []; + keyValueFilters[kvConfig.key].push(`${key.split(".")[1]}:${value}`); return; } const config = configuration.keys?.find(k => k.key === key); if (!config) return; - const processedValue = Array.isArray(value) - ? (value as string[]).filter(v => v !== null) - : config?.valueType === "multi-select" - ? ((value as string) ?? "").split(",") - : ((value as string) ?? ""); - - filtersMap.set(key, createFilter(key, config, processedValue)); + filtersMap.set(key, createFilter(key, config, + Array.isArray(value) + ? (value as string[]).filter(v => v !== null) + : config?.valueType === "multi-select" + ? ((value as string) ?? "").split(",") + : ((value as string) ?? "") + )); }); - if (details.length > 0) { - const config = configuration.keys?.find(k => k.key === "details"); + Object.entries(keyValueFilters).forEach(([key, values]) => { + const config = configuration.keys?.find(k => k.key === key); if (config) { - filtersMap.set("details", createFilter("details", config, details)); + filtersMap.set(key, createFilter(key, config, values)); } - } + }); if (route.query.startDate && route.query.endDate) { const timeRangeConfig = configuration.keys?.find(k => k.key === "timeRange"); @@ -227,13 +233,10 @@ export function useFilters( return Array.from(filtersMap.values()); }; - const isKVFilter = (field: string, comparator: Comparators) => - field === "details" || (field === "labels" && KV_COMPARATORS.includes(comparator)); - - const processFieldValue = (config: any, params: any[], field: string, comparator: Comparators) => { + const processFieldValue = (config: any, params: any[], _field: string, comparator: Comparators) => { const isTextOp = TEXT_COMPARATORS.includes(comparator); - if (isKVFilter(field, comparator)) { + if (config?.valueType === "key-value") { const combinedValue = params.map(p => p?.value as string); return { value: combinedValue, @@ -253,10 +256,9 @@ export function useFilters( }; } - const param = params[0]; - let value = Array.isArray(param?.value) - ? param.value[0] - : (param?.value as string); + let value = Array.isArray(params[0]?.value) + ? params[0].value[0] + : (params[0]?.value as string); if (config?.valueType === "date" && typeof value === "string") { value = new Date(value); diff --git a/ui/src/components/filter/configurations/executionFilter.ts b/ui/src/components/filter/configurations/executionFilter.ts index e7e9e07caa..1289c3da7a 100644 --- a/ui/src/components/filter/configurations/executionFilter.ts +++ b/ui/src/components/filter/configurations/executionFilter.ts @@ -127,7 +127,7 @@ export const useExecutionFilter = (): ComputedRef => { label: t("filter.labels_execution.label"), description: t("filter.labels_execution.description"), comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS], - valueType: "text", + valueType: "key-value", }, { key: "triggerExecutionId", diff --git a/ui/src/components/filter/configurations/flowExecutionFilter.ts b/ui/src/components/filter/configurations/flowExecutionFilter.ts index 27a1844838..ed020adb4d 100644 --- a/ui/src/components/filter/configurations/flowExecutionFilter.ts +++ b/ui/src/components/filter/configurations/flowExecutionFilter.ts @@ -74,7 +74,7 @@ export const useFlowExecutionFilter = (): ComputedRef => { label: t("filter.labels_execution.label"), description: t("filter.labels_execution.description"), comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS], - valueType: "text", + valueType: "key-value", }, { key: "triggerExecutionId", diff --git a/ui/src/components/filter/configurations/flowFilter.ts b/ui/src/components/filter/configurations/flowFilter.ts index d5636f965e..3ed407aa9f 100644 --- a/ui/src/components/filter/configurations/flowFilter.ts +++ b/ui/src/components/filter/configurations/flowFilter.ts @@ -67,7 +67,7 @@ export const useFlowFilter = (): ComputedRef => { label: t("filter.labels_flow.label"), description: t("filter.labels_flow.description"), comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS], - valueType: "text", + valueType: "key-value", }, ] }; diff --git a/ui/src/components/filter/utils/helpers.ts b/ui/src/components/filter/utils/helpers.ts index 513493195d..ef7242fab8 100644 --- a/ui/src/components/filter/utils/helpers.ts +++ b/ui/src/components/filter/utils/helpers.ts @@ -25,7 +25,7 @@ export const decodeSearchParams = (query: LocationQuery) => const [, field, operation, subKey] = match; - if (field === "labels" && subKey) { + if (subKey) { return { field, value: `${subKey}:${decodeURIComponentSafely(value)}`, @@ -57,30 +57,19 @@ export const encodeFiltersToQuery = (filters: Filter[], keyOfComparator: (compar query[`filters[${key}][${comparatorKey}]`] = value?.toString() ?? ""; } return query; - case "labels": - if (Array.isArray(value)) { - value.forEach((label: string) => { - const [k, v] = label.split(":", 2); - if (k && v) query[`filters[labels][${comparatorKey}][${k}]`] = v; - }); - } else if (typeof value === "string") { - const [k, v] = value.split(":", 2); - if (k && v) { - query[`filters[labels][${comparatorKey}][${k}]`] = v; - } else { - query[`filters[${key}][${comparatorKey}]`] = value; - } - } - return query; default: { - const processedValue = Array.isArray(value) - ? value.join(",") - : typeof value === "object" && "startDate" in value - ? `${value.startDate.toISOString()},${value.endDate.toISOString()}` + if (Array.isArray(value) && value.some(v => typeof v === "string" && v.includes(":"))) { + value.forEach((item: string) => { + const [k, v] = item.split(":", 2); + if (k && v) query[`filters[${key}][${comparatorKey}][${k}]`] = v; + }); + } else { + query[`filters[${key}][${comparatorKey}]`] = Array.isArray(value) + ? value.join(",") : value instanceof Date ? value.toISOString() - : value; - query[`filters[${key}][${comparatorKey}]`] = processedValue?.toString() ?? ""; + : value?.toString() ?? ""; + } return query; } } diff --git a/ui/src/components/layout/Labels.vue b/ui/src/components/layout/Labels.vue index a52700d719..eb4bff3f79 100644 --- a/ui/src/components/layout/Labels.vue +++ b/ui/src/components/layout/Labels.vue @@ -26,8 +26,16 @@ } const props = withDefaults( - defineProps<{ labels: Label[]; readOnly?: boolean }>(), - {labels: () => [], readOnly: false}, + defineProps<{ + labels?: Label[]; + readOnly?: boolean; + filterType?: "labels" | "metadata"; + }>(), + { + labels: () => [], + readOnly: false, + filterType: "labels", + }, ); import {decodeSearchParams} from "../../components/filter/utils/helpers"; @@ -48,7 +56,7 @@ }; const updateLabel = (label: Label) => { - const getKey = (key: string) => `filters[labels][EQUALS][${key}]`; + const getKey = (key: string) => `filters[${props.filterType}][EQUALS][${key}]`; if (isChecked(label)) { const replacementQuery = {...route.query}; diff --git a/ui/src/components/layout/TopNavBar.vue b/ui/src/components/layout/TopNavBar.vue index d3d184c917..6ca2306d99 100644 --- a/ui/src/components/layout/TopNavBar.vue +++ b/ui/src/components/layout/TopNavBar.vue @@ -33,6 +33,11 @@ @click="onStarClick" /> +
+ + {{ longDescription }} + +
@@ -77,15 +82,20 @@ const props = defineProps<{ title: string; description?: string; - breadcrumb?: { label: string; link?: RouterLinkTo; disabled?: boolean }[]; + longDescription?: string; + breadcrumb?: { + label: string; + link?: RouterLinkTo; + disabled?: boolean; + }[]; beta?: boolean; }>(); - const logsStore = useLogsStore(); - const bookmarksStore = useBookmarksStore(); - const flowStore = useFlowStore(); const route = useRoute(); + const logsStore = useLogsStore(); + const flowStore = useFlowStore(); const layoutStore = useLayoutStore(); + const bookmarksStore = useBookmarksStore(); const shouldDisplayDeleteButton = computed(() => { @@ -182,6 +192,12 @@ align-items: center; } + .description { + font-size: 0.875rem; + margin-top: -0.5rem; + color: var(--ks-content-secondary); + } + .icon { border: none; color: var(--ks-content-tertiary); diff --git a/ui/src/components/layout/empty/assets/visuals/assets.png b/ui/src/components/layout/empty/assets/visuals/assets.png new file mode 100644 index 0000000000..459d4b7aa8 Binary files /dev/null and b/ui/src/components/layout/empty/assets/visuals/assets.png differ diff --git a/ui/src/components/layout/empty/images.ts b/ui/src/components/layout/empty/images.ts index 67963464b6..2b74c97605 100644 --- a/ui/src/components/layout/empty/images.ts +++ b/ui/src/components/layout/empty/images.ts @@ -8,6 +8,7 @@ import plugins from "./assets/visuals/plugins.png"; import triggers from "./assets/visuals/triggers.png"; import versionPlugin from "./assets/visuals/versionPlugin.png"; import panels from "./assets/visuals/panels.png"; +import assets from "./assets/visuals/assets.png"; export const images: Record = { announcements, @@ -18,8 +19,10 @@ export const images: Record = { "dependencies.FLOW": dependencies, "dependencies.EXECUTION": dependencies, "dependencies.NAMESPACE": dependencies, + "dependencies.ASSET": dependencies, plugins, triggers, versionPlugin, panels, + assets }; diff --git a/ui/src/override/components/useLeftMenu.ts b/ui/src/override/components/useLeftMenu.ts index b275510e98..bb2e641ce1 100644 --- a/ui/src/override/components/useLeftMenu.ts +++ b/ui/src/override/components/useLeftMenu.ts @@ -15,7 +15,7 @@ import ContentCopy from "vue-material-design-icons/ContentCopy.vue"; import PlayOutline from "vue-material-design-icons/PlayOutline.vue"; import FileDocumentOutline from "vue-material-design-icons/FileDocumentOutline.vue"; import FlaskOutline from "vue-material-design-icons/FlaskOutline.vue"; -// import PackageVariantClosed from "vue-material-design-icons/PackageVariantClosed.vue"; +import PackageVariantClosed from "vue-material-design-icons/PackageVariantClosed.vue"; import FolderOpenOutline from "vue-material-design-icons/FolderOpenOutline.vue"; import PuzzleOutline from "vue-material-design-icons/PuzzleOutline.vue"; import ShapePlusOutline from "vue-material-design-icons/ShapePlusOutline.vue"; @@ -145,8 +145,19 @@ export function useLeftMenu() { locked: true, }, }, - // TODO: To add Assets entry here in future release - // Uncomment PackageVariantClosed on line 25 and use as the icon + { + title: t("demos.assets.label"), + routes: routeStartWith("assets"), + href: { + name: "assets/list" + }, + icon: { + element: PackageVariantClosed, + }, + attributes: { + locked: true, + }, + }, { title: t("namespaces"), routes: routeStartWith("namespaces"), diff --git a/ui/src/routes/routes.js b/ui/src/routes/routes.js index 4d7cff2da6..82cf55dbe1 100644 --- a/ui/src/routes/routes.js +++ b/ui/src/routes/routes.js @@ -7,6 +7,7 @@ import DemoAuditLogs from "../components/demo/AuditLogs.vue" import DemoInstance from "../components/demo/Instance.vue" import DemoApps from "../components/demo/Apps.vue" import DemoTests from "../components/demo/Tests.vue" +import DemoAssets from "../components/demo/Assets.vue" import {applyDefaultFilters} from "../components/filter/composables/useDefaultFilter"; export default [ @@ -123,6 +124,7 @@ export default [ //Demo Pages {name: "apps/list", path: "/:tenant?/apps", component: DemoApps}, {name: "tests/list", path: "/:tenant?/tests", component: DemoTests}, + {name: "assets/list", path: "/:tenant?/assets", component: DemoAssets}, {name: "admin/iam", path: "/:tenant?/admin/iam", component: DemoIAM}, {name: "admin/tenants/list", path: "/:tenant?/admin/tenants", component: DemoTenants}, {name: "admin/auditlogs/list", path: "/:tenant?/admin/auditlogs", component: DemoAuditLogs}, diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json index 715fa8cc13..3f4ecc2cee 100644 --- a/ui/src/translations/en.json +++ b/ui/src/translations/en.json @@ -896,6 +896,12 @@ "title": "Ensure Reliability with Every Change", "message": "Verify the logic of your flows in isolation, detect regressions early, and maintain confidence in your automations as they change and grow." }, + "assets": { + "label": "Assets", + "header": "Assets Metadata and Observability", + "title": "Bring every dataset, service, and dependency into view.", + "message": "Assets connect observability, lineage, and ownership metadata so platform teams can troubleshoot faster and deploy with confidence." + }, "IAM": { "title": "Manage Users through IAM with SSO, SCIM and RBAC", "message": "Kestra Enterprise Edition has built-in IAM capabilities with single sign-on (SSO), SCIM directory sync, and role-based access control (RBAC), integrating with multiple identity providers and letting you assign fine-grained permissions for users and service accounts." @@ -1370,6 +1376,10 @@ "title": "You have no Tests yet!", "content": "Add tests to validate quality and avoid regressions in your flows." }, + "assets": { + "title": "You have no Assets yet!", + "content": "Add assets to track and manage your data assets, services, and infrastructure." + }, "concurrency_executions": { "title": "No ongoing Executions for this Flow.", "content": "Read more about Executions in our documentation." @@ -1390,6 +1400,10 @@ "NAMESPACE": { "title": "There are currently no dependencies.", "content": "Read more about Namespace Dependencies in our documentation." + }, + "ASSET": { + "title": "There are currently no dependencies.", + "content": "This asset has no upstream or downstream dependencies with flows or other assets." } }, "plugins": { @@ -1430,7 +1444,8 @@ "flows": "No Flows Found", "kv_pairs": "No Key-Value pairs Found", "secrets": "No Secrets Found", - "templates": "No Templates Found" + "templates": "No Templates Found", + "assets": "No Assets Found" }, "duplicate-pair": "{label} \"{key}\" is duplicated, first key ignored.", "dashboards": { @@ -1575,6 +1590,7 @@ }, "search": { "placeholder": "Search by flow or namespace...", + "asset_placeholder": "Search by asset or flow or namespace...", "no_results": "No results found for {term}" } }, diff --git a/ui/src/utils/constants.ts b/ui/src/utils/constants.ts index 7917a82248..2aa2c42c8d 100644 --- a/ui/src/utils/constants.ts +++ b/ui/src/utils/constants.ts @@ -20,6 +20,8 @@ export const storageKeys = { DISPLAY_KV_COLUMNS: "displayKvColumns", DISPLAY_SECRETS_COLUMNS: "displaySecretsColumns", DISPLAY_TRIGGERS_COLUMNS: "displayTriggersColumns", + DISPLAY_ASSETS_COLUMNS: "displayAssetsColumns", + DISPLAY_ASSET_EXECUTIONS_COLUMNS: "displayAssetExecutionsColumns", SELECTED_TENANT: "selectedTenant", EXECUTE_FLOW_BEHAVIOUR: "executeFlowBehaviour", SHOW_CHART: "showChart",