diff --git a/dashboard/package.json b/dashboard/package.json
index b09a612e..acd01422 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -5,6 +5,7 @@
"node": ">=20"
},
"dependencies": {
+ "@krumio/trailhand-ui": "^1.4.1",
"@rancher/shell": "3.0.7",
"@types/lodash": "^4.17.16",
"eslint": "^9.28.0",
@@ -39,6 +40,8 @@
"@types/vue": "^1.0.31",
"@vue/eslint-config-standard": "5.1.2",
"@vue/eslint-config-typescript": "^11.0.3",
+ "esbuild-loader": "^4.4.2",
"globals": "^16.2.0"
- }
+ },
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
diff --git a/dashboard/pkg/epinio/components/tables/types.ts b/dashboard/pkg/epinio/components/tables/types.ts
index 0768f30b..42a5b2d6 100644
--- a/dashboard/pkg/epinio/components/tables/types.ts
+++ b/dashboard/pkg/epinio/components/tables/types.ts
@@ -44,6 +44,34 @@ export const dataTableFormatters = {
dateTime: (value: any): string => {
if (!value) return '-';
return new Date(value).toLocaleString();
+ },
+
+ /**
+ * Format memory bytes to human readable format (B, KB, MB, GB, TB)
+ */
+ memory: (value: any): string => {
+ if (!value || value === 0) return '0 B';
+
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ let val = Number(value);
+ let unitIndex = 0;
+
+ while (val >= 1024 && unitIndex < units.length - 1) {
+ val = val / 1024;
+ unitIndex++;
+ }
+
+ return `${Math.round(val)} ${units[unitIndex]}`;
+ },
+
+ /**
+ * Format millicpus value
+ */
+ milliCPUs: (value: any): string => {
+ if (!value || value === 0) return '0';
+
+ const formatted = Math.round(Number(value)).toString();
+ return formatted === '0' ? '0' : formatted;
}
};
@@ -83,6 +111,22 @@ export interface DataTableColumn {
*/
formatter?: string | ((value: any, row: DataTableRow) => any);
+ /**
+ * Link URL for the column. Can be:
+ * - A string field name to access the URL from row data
+ * - A function (row) => URL string
+ * When provided, the cell value will be rendered as a clickable link
+ * @param row - The entire row object
+ * @returns The URL for the link
+ */
+ link?: string | ((row: DataTableRow) => string | null | undefined);
+
+ /**
+ * Target attribute for links (default: '_self')
+ * Use '_blank' to open links in a new tab
+ */
+ linkTarget?: '_self' | '_blank' | '_parent' | '_top';
+
/**
* Custom sort function for this column
* @param a - First row to compare
diff --git a/dashboard/pkg/epinio/detail/applications.vue b/dashboard/pkg/epinio/detail/applications.vue
index 0717b8b2..8e31fff6 100644
--- a/dashboard/pkg/epinio/detail/applications.vue
+++ b/dashboard/pkg/epinio/detail/applications.vue
@@ -1,5 +1,7 @@
@@ -448,51 +581,7 @@ const commitPosition = computed(() => {
>
{{ t('epinio.applications.detail.deployment.commits.redeploy') }}
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -508,57 +597,21 @@ const commitPosition = computed(() => {
name="instances"
:weight="3"
>
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
diff --git a/dashboard/pkg/epinio/detail/catalogservices.vue b/dashboard/pkg/epinio/detail/catalogservices.vue
index 39149477..76895847 100644
--- a/dashboard/pkg/epinio/detail/catalogservices.vue
+++ b/dashboard/pkg/epinio/detail/catalogservices.vue
@@ -1,28 +1,50 @@
@@ -64,44 +97,7 @@ const columns: DataTableColumn[] = [
{{ t('epinio.catalogService.detail.servicesTitle', { catalogService: props.value.name }) }}
-
-
-
-
-
-
- {{ row.catalog_service }}
-
-
-
-
-
- ,
-
-
-
-
-
-
+ Loading...
+
diff --git a/dashboard/pkg/epinio/list/appcharts.vue b/dashboard/pkg/epinio/list/appcharts.vue
index b605a71b..05de8b4d 100644
--- a/dashboard/pkg/epinio/list/appcharts.vue
+++ b/dashboard/pkg/epinio/list/appcharts.vue
@@ -1,32 +1,24 @@
-
-
-
-
-
+ Loading...
+
diff --git a/dashboard/pkg/epinio/list/configurations.vue b/dashboard/pkg/epinio/list/configurations.vue
index 5a7506fd..0c244f48 100644
--- a/dashboard/pkg/epinio/list/configurations.vue
+++ b/dashboard/pkg/epinio/list/configurations.vue
@@ -1,19 +1,89 @@
-
-
-
-
-
-
-
-
-
-
-
-
- ,
-
-
-
-
-
+ Loading...
+
diff --git a/dashboard/pkg/epinio/list/namespaces.vue b/dashboard/pkg/epinio/list/namespaces.vue
index 11648e0e..889f9f5c 100644
--- a/dashboard/pkg/epinio/list/namespaces.vue
+++ b/dashboard/pkg/epinio/list/namespaces.vue
@@ -1,4 +1,6 @@
-
-
-
-
-
-
- {{ row.catalog_service }}
-
-
-
-
-
- ,
-
-
-
-
-
+ Loading...
+
diff --git a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue
index de4e0dbc..d102fb81 100644
--- a/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue
+++ b/dashboard/pkg/epinio/pages/c/_cluster/applications/index.vue
@@ -1,20 +1,22 @@
@@ -178,43 +232,7 @@ const columns: DataTableColumn[] = [
@click="rediscover"
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.api }}
-
-
-
-
+
diff --git a/dashboard/pkg/epinio/utils/table-helpers.ts b/dashboard/pkg/epinio/utils/table-helpers.ts
new file mode 100644
index 00000000..a717d69c
--- /dev/null
+++ b/dashboard/pkg/epinio/utils/table-helpers.ts
@@ -0,0 +1,140 @@
+/**
+ * Helper utilities for working with trailhand-ui data-table component
+ */
+
+import type { Router } from 'vue-router';
+import type { Store } from 'vuex';
+
+/**
+ * Create a link resolver function for data-table columns
+ * This resolves Vue Router route objects to URL strings
+ * @param router - Vue Router instance
+ * @param locationProperty - Property path to the route location (e.g., 'detailLocation', 'namespaceLocation')
+ * @returns A function that can be used as a column's link property
+ */
+export function createLinkResolver(router: Router, locationProperty: string) {
+ return (row: any) => {
+ // Support nested properties like 'service.detailLocation'
+ const location = locationProperty.split('.').reduce((obj, key) => obj?.[key], row);
+
+ if (location) {
+ try {
+ const resolved = router.resolve(location);
+ return resolved.href;
+ } catch (e) {
+ console.error(`Failed to resolve route for ${locationProperty}:`, e);
+ return null;
+ }
+ }
+ return null;
+ };
+}
+
+/**
+ * Create a data-table element with the given configuration
+ * @param columns - Column definitions
+ * @param rows - Data rows
+ * @param options - Additional table options
+ * @returns The created table element
+ */
+export function createDataTable(
+ columns: any[],
+ rows: any[],
+ options: {
+ rowActions?: boolean;
+ searchable?: boolean;
+ sortable?: boolean;
+ paginated?: boolean;
+ rowsPerPage?: number;
+ } = {}
+): HTMLElement {
+ const tableElement = document.createElement('data-table');
+
+ (tableElement as any).columns = columns;
+ (tableElement as any).rows = rows;
+ (tableElement as any).rowActions = options.rowActions ?? true;
+ (tableElement as any).searchable = options.searchable ?? true;
+ (tableElement as any).sortable = options.sortable ?? true;
+ (tableElement as any).paginated = options.paginated ?? true;
+
+ if (options.rowsPerPage) {
+ (tableElement as any).rowsPerPage = options.rowsPerPage;
+ }
+
+ return tableElement;
+}
+
+/**
+ * Handle action menu clicks
+ * @param action - The action object with action method name
+ * @param resource - The resource object
+ */
+export function handleActionClick(action: any, resource: any): void {
+ const actionMethod = action.action;
+ if (resource && actionMethod && typeof resource[actionMethod] === 'function') {
+ resource[actionMethod]();
+ }
+}
+
+/**
+ * Setup action click event listener on a table element
+ * @param tableElement - The table element
+ * @param handler - Optional custom handler, defaults to handleActionClick
+ */
+export function setupActionListener(
+ tableElement: HTMLElement,
+ handler: (action: any, resource: any) => void = handleActionClick
+): void {
+ tableElement.addEventListener('action-click', ((event: CustomEvent) => {
+ const { action, resource } = event.detail;
+ handler(action, resource);
+ }) as EventListener);
+}
+
+/**
+ * Setup navigation event listener on a table element
+ * This handles link clicks and navigates using Vue Router instead of page reloads
+ * @param tableElement - The table element
+ * @param router - Vue Router instance
+ */
+export function setupNavigationListener(
+ tableElement: HTMLElement,
+ router: Router
+): void {
+ tableElement.addEventListener('navigate', ((event: CustomEvent) => {
+ const { url } = event.detail;
+ if (url) {
+ router.push(url);
+ }
+ }) as EventListener);
+}
+
+/**
+ * Apply namespace filtering to a list of rows
+ * Uses the Vuex store's activeNamespaceCache to filter rows by namespace
+ * @param store - Vuex store instance
+ * @param allRows - All rows to filter
+ * @returns Filtered rows based on active namespace selection
+ */
+export function applyNamespaceFilter(store: Store, allRows: any[]): any[] {
+ // Access the cache key to trigger reactivity on namespace filter changes
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const cacheKey = store.state.activeNamespaceCacheKey;
+ const activeNamespaces = store.state.activeNamespaceCache;
+
+ // If no namespace filter exists, or if it's empty (no namespaces loaded yet), show all rows
+ if (!activeNamespaces || Object.keys(activeNamespaces).length === 0) {
+ return allRows;
+ }
+
+ // Filter rows by namespace
+ return allRows.filter((row: any) => {
+ const namespace = row.meta?.namespace;
+ // If row has no namespace, show it by default
+ if (!namespace) {
+ return true;
+ }
+ // Check if this namespace is in the active filter
+ return activeNamespaces[namespace];
+ });
+}
diff --git a/dashboard/vue.config.js b/dashboard/vue.config.js
index cea72163..06a801b8 100644
--- a/dashboard/vue.config.js
+++ b/dashboard/vue.config.js
@@ -1,6 +1,22 @@
const config = require('@rancher/shell/vue.config'); // eslint-disable-line @typescript-eslint/no-var-requires
-module.exports = config(__dirname, {
+const baseConfig = config(__dirname, {
excludes: [],
// excludes: ['fleet', 'example']
});
+
+// custom element configuration for web components
+baseConfig.chainWebpack = (config) => {
+ config.module
+ .rule('vue')
+ .use('vue-loader')
+ .tap(options => ({
+ ...options,
+ compilerOptions: {
+ ...(options.compilerOptions || {}),
+ isCustomElement: tag => tag === 'data-table' || tag === 'action-menu' || tag === 'iconify-icon'
+ }
+ }));
+};
+
+module.exports = baseConfig;