feat(assets): introducing assets ui

This commit is contained in:
Piyush Bhaskar
2025-12-29 17:31:50 +05:30
committed by Loïc Mathieu
parent 156a75c668
commit 8dff5855f2
23 changed files with 284 additions and 107 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

View File

@@ -0,0 +1,34 @@
<template>
<TopNavBar :title="routeInfo.title" />
<Layout
:title="t(`demos.assets.title`)"
:image="{
source: img,
alt: t(`demos.assets.title`)
}"
:video="{
//TODO: replace with ASSET video
source: 'https://www.youtube.com/embed/jMZ9Cs3xxpo',
}"
>
<template #message>
{{ $t(`demos.assets.message`) }}
</template>
</Layout>
</template>
<script setup lang="ts">
import {computed} from "vue";
import {useI18n} from "vue-i18n";
import img from "../../assets/demo/assets.png";
import useRouteContext from "../../composables/useRouteContext";
import Layout from "./Layout.vue";
import TopNavBar from "../../components/layout/TopNavBar.vue";
const {t} = useI18n();
const routeInfo = computed(() => ({title: t("demos.assets.header")}));
useRouteContext(routeInfo);
</script>

View File

@@ -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;
}
}

View File

@@ -42,6 +42,7 @@
:elements="getElements()"
@select="selectNode"
:selected="selectedNodeID"
:subtype="SUBTYPE"
/>
</el-splitter-panel>
</el-splitter>
@@ -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);
</script>
<style scoped lang="scss">
@@ -95,7 +110,7 @@
& .controls {
position: absolute;
bottom: 10px;
bottom: 16px;
left: 10px;
display: flex;
flex-direction: column;

View File

@@ -9,26 +9,32 @@
<script setup lang="ts">
import {computed} from "vue";
import {FLOW, EXECUTION, NAMESPACE, type Node} from "../utils/types";
import {FLOW, EXECUTION, NAMESPACE, ASSET, type Node} from "../utils/types";
const props = defineProps<{
node: Node;
subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE;
subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET;
}>();
const to = computed(() => {
const base = {namespace: props.node.namespace};
if ("id" in props.node.metadata && props.node.metadata.id)
if (props.subtype === ASSET) {
return {
name: "assets/update",
params: {...base, assetId: props.node.flow},
};
} else if ("id" in props.node.metadata && props.node.metadata.id) {
return {
name: "executions/update",
params: {...base, flowId: props.node.flow, id: props.node.metadata.id},
};
else
} else {
return {
name: "flows/update",
params: {...base, id: props.node.flow},
};
}
});
</script>

View File

@@ -2,7 +2,7 @@
<section id="input">
<el-input
v-model="search"
:placeholder="$t('dependency.search.placeholder')"
:placeholder="$t(props.subtype === ASSET ? 'dependency.search.asset_placeholder' : 'dependency.search.placeholder')"
clearable
/>
</section>
@@ -38,10 +38,13 @@
size="small"
/>
<RouterLink
v-if="[FLOW, NAMESPACE].includes(row.data.metadata.subtype)"
v-if="[FLOW, NAMESPACE, ASSET].includes(row.data.metadata.subtype)"
:to="{
name: 'flows/update',
params: {namespace: row.data.namespace, id: row.data.flow}}"
name: row.data.metadata.subtype === ASSET ? 'assets/update' : 'flows/update',
params: row.data.metadata.subtype === ASSET
? {namespace: row.data.namespace, assetId: row.data.flow}
: {namespace: row.data.namespace, id: row.data.flow}
}"
>
<el-icon :size="16">
<OpenInNew />
@@ -64,12 +67,13 @@
import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
import {NODE, FLOW, EXECUTION, NAMESPACE, type Node} from "../utils/types";
import {NODE, FLOW, EXECUTION, NAMESPACE, ASSET, type Node} from "../utils/types";
const emits = defineEmits<{ (e: "select", id: Node["id"]): void }>();
const props = defineProps<{
elements: cytoscape.ElementDefinition[];
selected: Node["id"] | undefined;
subtype?: typeof FLOW | typeof EXECUTION | typeof NAMESPACE | typeof ASSET;
}>();
const focusSelectedRow = () => {
@@ -177,6 +181,10 @@ section#row {
& section#right {
flex-shrink: 0;
margin-left: 0.5rem;
:deep(a:hover .el-icon) {
color: var(--ks-content-link-hover);
}
}
}
</style>

View File

@@ -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 nodes own ID).
*/
function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref<Node["id"] | undefined>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE, id?: Node["id"]): void {
function selectHandler(cy: cytoscape.Core, node: cytoscape.NodeSingular, selected: Ref<Node["id"] | undefined>, 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<HTMLElement | null>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE = FLOW, initialNodeID: string, params: RouteParams, isTesting = false) {
export function useDependencies(
container: Ref<HTMLElement | null>,
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<HTMLElement | null>, 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<HTMLElement | null>, 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<HTMLElement | null>, 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<HTMLElement | null>, 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),
];
}

View File

@@ -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 = {

View File

@@ -152,6 +152,8 @@
font-size: 12px;
color: var(--ks-content-primary);
white-space: nowrap;
display: flex;
align-items: center;
}
.value {
font-weight: 700;

View File

@@ -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(() => {

View File

@@ -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<string, any>) => {
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<string, any>) => {
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<string, AppliedFilter>();
const details: string[] = [];
const keyValueFilters: Record<string, string[]> = {};
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);

View File

@@ -127,7 +127,7 @@ export const useExecutionFilter = (): ComputedRef<FilterConfiguration> => {
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",

View File

@@ -74,7 +74,7 @@ export const useFlowExecutionFilter = (): ComputedRef<FilterConfiguration> => {
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",

View File

@@ -67,7 +67,7 @@ export const useFlowFilter = (): ComputedRef<FilterConfiguration> => {
label: t("filter.labels_flow.label"),
description: t("filter.labels_flow.description"),
comparators: [Comparators.EQUALS, Comparators.NOT_EQUALS],
valueType: "text",
valueType: "key-value",
},
]
};

View File

@@ -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;
}
}

View File

@@ -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};

View File

@@ -33,6 +33,11 @@
@click="onStarClick"
/>
</h1>
<div class="description">
<slot name="description">
{{ longDescription }}
</slot>
</div>
</div>
</div>
</div>
@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -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<string, string> = {
announcements,
@@ -18,8 +19,10 @@ export const images: Record<string, string> = {
"dependencies.FLOW": dependencies,
"dependencies.EXECUTION": dependencies,
"dependencies.NAMESPACE": dependencies,
"dependencies.ASSET": dependencies,
plugins,
triggers,
versionPlugin,
panels,
assets
};

View File

@@ -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"),

View File

@@ -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},

View File

@@ -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 <strong><a href=\"https://kestra.io/docs/workflow-components/execution\" target=\"_blank\">Executions</a></strong> in our documentation."
@@ -1390,6 +1400,10 @@
"NAMESPACE": {
"title": "There are currently no dependencies.",
"content": "Read more about <a href=\"https://kestra.io/docs/ui/namespaces#dependencies\" target=\"_blank\">Namespace Dependencies</a> 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}"
}
},

View File

@@ -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",