=> {
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",