Fleet enable HelmOps Graph

- Make gitRepoGraphConfig generic
- Make ForceDirectedTreeChart generic, remove Fleet references
- Remove Fleet references from ResourceDetail
- Add graph permissions check in ForceDirectedTreeChart
- Dashboard fixing filters when changing view mode

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia 2025-05-14 16:19:28 +02:00
parent 6b92c9d377
commit 7b2d0f0a6e
No known key found for this signature in database
GPG Key ID: E6D011B7415D4393
11 changed files with 361 additions and 352 deletions

View File

@ -34,6 +34,6 @@ export default class FleetApplicationDetailsPo extends PagePo {
}
graph() {
return this.self().find('[data-testid="gitrepo_graph"]');
return this.self().find('[data-testid="resource-graph"]');
}
}

View File

@ -145,6 +145,6 @@ export class FleetGitRepoDetailsPo extends BaseDetailPagePo {
}
graph() {
return this.self().find('[data-testid="gitrepo_graph"]');
return this.self().find('[data-testid="resource-graph"]');
}
}

View File

@ -153,6 +153,11 @@ generic:
opensInNewTab: Opens in a new tab
graph:
noPermissions: You do not have permission to Graph view
loading: Loading chart data...
rendering: Rendering chart...
locale:
menu: Locale selector menu
en-us: English
@ -2791,7 +2796,9 @@ fleet:
title: Release
label: Name
placeholder: placeholder
registryOption: Choose a Rancher Registry
registry:
option: Choose a Rancher Registry
error: The {registry} has errors, please try to update the registry in Rancher repository.
values:
title: Values
selectLabel: Values
@ -2806,6 +2813,7 @@ fleet:
diff: Compare Changes
values:
selectLabel: YAML
loading: Loading from chart
valuesFiles:
selectLabel: Files
ariaLabel: Enter path for value File
@ -2860,8 +2868,6 @@ fleet:
other {Matches {matched, number} of {total, number} existing clusters, including "{sample}"}
}
fdc:
loadingChart: Loading chart data...
renderingChart: Rendering chart...
id: ID
type: Type
state: State

View File

@ -112,6 +112,10 @@ generic:
other {匹配 {total, number} 中的 {matched, number} 个,包括 "{sample}"}
}
graph:
loading: 正在加载 Chart 数据...
rendering: 正在渲染 Chart...
locale:
en-us: English
zh-hans: 简体中文
@ -2221,8 +2225,6 @@ fleet:
other {现有{total, number}个集群,与其中的{matched, number}个匹配,包括"{sample}"}
}
fdc:
loadingChart: 正在加载 Chart 数据...
renderingChart: 正在渲染 Chart...
id: ID
type: 类型
state: 状态

View File

@ -2,7 +2,6 @@
import * as d3 from 'd3';
import { STATES } from '@shell/plugins/dashboard-store/resource-class';
import { BadgeState } from '@components/BadgeState';
import { getChartIcon } from './chartIcons.js';
export default {
name: 'ForceDirectedTreeChart',
@ -17,8 +16,24 @@ export default {
required: true
}
},
async fetch() {
this.canViewChart = await this.fdcConfig.checkSchemaPermissions(this.$store);
if (this.canViewChart) {
// set watcher for the chart data
this.dataWatcher = this.$watch(this.fdcConfig.watcherProp, (newValue) => {
this.watcherFunction(newValue);
}, {
deep: true,
immediate: true
});
}
},
data() {
return {
canViewChart: null,
dataWatcher: undefined,
parsedInfo: undefined,
root: undefined,
@ -37,7 +52,7 @@ export default {
},
methods: {
watcherFunction(newValue) {
if (newValue.length) {
if (newValue?.length) {
if (!this.isChartFirstRendered) {
this.parsedInfo = this.fdcConfig.parseData(this.data);
@ -158,16 +173,11 @@ export default {
.attr('r', this.setNodeRadius);
nodeEnter.append('circle')
.attr('r', (d) => {
return this.setNodeRadius(d) - 5;
})
.attr('r', (d) => this.setNodeRadius(d) - 5)
.attr('class', 'node-hover-layer');
nodeEnter.append('svg').html((d) => {
const icon = this.fdcConfig.fetchNodeIcon(d);
return getChartIcon(icon);
})
nodeEnter.append('svg')
.html((d) => this.fdcConfig.fetchNodeIcon(d))
.attr('x', this.nodeImagePosition)
.attr('y', this.nodeImagePosition)
.attr('height', this.nodeImageSize)
@ -177,9 +187,7 @@ export default {
this.simulation.nodes(this.allNodesData);
this.simulation.force('link', d3.forceLink()
.id((d) => {
return d.id;
})
.id((d) => d.id)
.distance(100)
.links(this.allLinks)
);
@ -188,10 +196,10 @@ export default {
const lowerCaseStatus = d.data?.state ? d.data.state.toLowerCase() : 'unkown_status';
const defaultClassArray = ['node'];
if (STATES[lowerCaseStatus] && STATES[lowerCaseStatus].color) {
defaultClassArray.push(`node-${ STATES[lowerCaseStatus].color }`);
} else {
if (d?.data?.muteStatus) {
defaultClassArray.push(`node-default-fill`);
} else if (STATES[lowerCaseStatus] && STATES[lowerCaseStatus].color) {
defaultClassArray.push(`node-${ STATES[lowerCaseStatus].color }`);
}
// node active (clicked)
@ -334,14 +342,6 @@ export default {
this.svg = d3.select('#tree').append('svg')
.attr('viewBox', `0 0 ${ this.fdcConfig.chartWidth } ${ this.fdcConfig.chartHeight }`)
.attr('preserveAspectRatio', 'none');
// set watcher for the chart data
this.dataWatcher = this.$watch(this.fdcConfig.watcherProp, function(newValue) {
this.watcherFunction(newValue);
}, {
deep: true,
immediate: true
});
},
unmounted() {
this.dataWatcher();
@ -353,20 +353,26 @@ export default {
<div>
<div
class="chart-container"
data-testid="gitrepo_graph"
data-testid="resource-graph"
>
<!-- loading status container -->
<div
v-if="!isChartFirstRenderAnimationFinished"
class="loading-container"
>
<p v-show="!isChartFirstRendered">
{{ t('fleet.fdc.loadingChart') }}
<p v-if="canViewChart === false">
{{ t('graph.noPermissions') }}
</p>
<p v-show="isChartFirstRendered && !isChartFirstRenderAnimationFinished">
{{ t('fleet.fdc.renderingChart') }}
<p v-else-if="!isChartFirstRendered">
{{ t('graph.loading') }}
</p>
<i class="mt-10 icon-spinner icon-spin" />
<p v-else-if="!isChartFirstRenderAnimationFinished">
{{ t('graph.rendering') }}
</p>
<i
v-if="canViewChart !== false"
class="mt-10 icon-spinner icon-spin"
/>
</div>
<!-- main div for svg container -->
<div id="tree" />
@ -416,6 +422,9 @@ export default {
>
<p>{{ item.value }}</p>
</td>
<td v-else-if="item.type === 'resource-type'">
{{ t(`typeLabel."${ item.valueKey }"`, { count: 1 }) }}
</td>
<!-- default template -->
<td v-else>
{{ item.value }}
@ -478,32 +487,29 @@ export default {
}
}
&.repo.active > circle {
transform: scale(1.2);
}
&.bundle.active > circle {
transform: scale(1.35);
}
&.bundle-deployment.active > circle {
&.bundledeployment.active > circle {
transform: scale(1.6);
}
&.node-default-fill > circle,
&.repo > circle {
&.node-default-fill > circle {
transform: scale(1.2);
fill: var(--muted);
}
&:not(.repo).node-success > circle {
&.node-success > circle {
fill: var(--success);
}
&:not(.repo).node-info > circle {
&.node-info > circle {
fill: var(--info);
}
&:not(.repo).node-warning > circle {
&.node-warning > circle {
fill: var(--warning);
}
&:not(.repo).node-error > circle {
&.node-error > circle {
fill: var(--error);
}

View File

@ -6,14 +6,13 @@ import {
_VIEW, _EDIT, _CLONE, _IMPORT, _STAGE, _CREATE,
AS, _YAML, _DETAIL, _CONFIG, _GRAPH, PREVIEW, MODE,
} from '@shell/config/query-params';
import { FLEET, SCHEMA } from '@shell/config/types';
import { SCHEMA } from '@shell/config/types';
import { createYaml } from '@shell/utils/create-yaml';
import Masthead from '@shell/components/ResourceDetail/Masthead';
import DetailTop from '@shell/components/DetailTop';
import { clone, diff } from '@shell/utils/object';
import IconMessage from '@shell/components/IconMessage';
import ForceDirectedTreeChart from '@shell/components/fleet/ForceDirectedTreeChart';
import { checkSchemasForFindAllHash } from '@shell/utils/auth';
import ForceDirectedTreeChart from '@shell/components/ForceDirectedTreeChart';
import { stringify } from '@shell/utils/error';
import { Banner } from '@components/Banner';
@ -172,28 +171,6 @@ export default {
yaml = createYaml(schemas, resourceType, data);
}
} else {
if ( as === _GRAPH ) {
const graphSchema = await checkSchemasForFindAllHash({
cluster: {
inStoreType: 'management',
type: FLEET.CLUSTER
},
bundle: {
inStoreType: 'management',
type: FLEET.BUNDLE,
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
},
bundleDeployment: {
inStoreType: 'management',
type: FLEET.BUNDLE_DEPLOYMENT
}
}, this.$store);
this.canViewChart = graphSchema.cluster && graphSchema.bundle && graphSchema.bundleDeployment;
}
let fqid = id;
if ( schema.attributes?.namespaced && namespace ) {
@ -296,7 +273,6 @@ export default {
value: null,
model: null,
notFound: null,
canViewChart: true,
canViewYaml: null,
errors: []
};
@ -493,7 +469,7 @@ export default {
</div>
<ForceDirectedTreeChart
v-if="isGraph && canViewChart"
v-if="isGraph"
:data="chartData"
:fdc-config="getGraphConfig"
/>

View File

@ -1,17 +0,0 @@
// This is to mitigate an issue where the SVG icons being imported from the project weren't being rendered on Firefox
// To know more about this technique, check this doc: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
export const getChartIcon = (type) => `<defs>
<!-- GIT REPO ICON -->
<svg id="git" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96" xml:space="preserve"><path class="st0" d="M92.138 43.888 52.018 3.77a5.918 5.918 0 0 0-8.369 0l-8.33 8.332L45.887 22.67a7.025 7.025 0 0 1 7.23 1.684 7.031 7.031 0 0 1 1.67 7.275l10.185 10.185a7.03 7.03 0 0 1 7.275 1.67 7.04 7.04 0 0 1 0 9.958 7.042 7.042 0 0 1-11.492-7.658l-9.5-9.499v24.997a7.09 7.09 0 0 1 1.861 1.331 7.042 7.042 0 1 1-7.65-1.537V35.849a7.04 7.04 0 0 1-3.822-9.234l-10.418-10.42-27.51 27.508a5.921 5.921 0 0 0 0 8.371l40.121 40.118a5.919 5.919 0 0 0 8.37 0l39.93-39.932a5.92 5.92 0 0 0 0-8.37z"/></svg>
<!-- GENERIC BUNDLE ICON -->
<svg id="bundle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#FFF"><path d="M16 3.2C8.931 3.2 3.2 8.931 3.2 16S8.931 28.8 16 28.8 28.8 23.069 28.8 16 23.069 3.2 16 3.2zm0 22.4c-5.302 0-9.6-4.298-9.6-9.6s4.298-9.6 9.6-9.6 9.6 4.298 9.6 9.6a9.6 9.6 0 0 1-9.6 9.6z"/><path d="m24.086 16-6.232-1.348.917-1.424-1.424.917-1.348-6.232-1.348 6.232-1.424-.917.917 1.424L7.912 16l6.232 1.348-.917 1.424 1.424-.917 1.348 6.232 1.348-6.232 1.424.917-.917-1.424L24.086 16zM16 16.814a.814.814 0 1 1 0-1.628.814.814 0 0 1 0 1.628z"/></svg>
<!-- HELM BUNDLE ICON -->
<svg id="helm" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><defs><style>.cls-1{fill:#fff}</style></defs><path class="cls-1" d="M136.53 121.135c-.573-.543-1.195-1.127-1.81-1.718-12.617-12.121-22.382-26.136-28.28-42.702-1.65-4.636-2.897-9.365-2.675-14.359.02-.473.02-.949.052-1.422.445-6.446 4.759-9.703 11.058-8.176a27.325 27.325 0 0 1 5.732 2.196c6.89 3.456 12.506 8.564 17.67 14.166A112.527 112.527 0 0 1 160 102.55a8.296 8.296 0 0 0 .39.86c.07.13.222.213.559.52a176.357 176.357 0 0 1 81.02-24.861c-.177-.876-.282-1.546-.448-2.2a112.494 112.494 0 0 1-2.653-36.957 84.075 84.075 0 0 1 4.445-21.764 31.326 31.326 0 0 1 5.476-10.17 15.687 15.687 0 0 1 3.164-2.822 7.026 7.026 0 0 1 8.032-.056 17.279 17.279 0 0 1 5.84 6.731 53.054 53.054 0 0 1 5.263 14.677 112.505 112.505 0 0 1 2.122 33.004 95.598 95.598 0 0 1-3.49 19.91c7.121 1.312 14.21 2.33 21.147 3.978a186.38 186.38 0 0 1 20.44 6.003 188.317 188.317 0 0 1 19.77 8.57c6.346 3.163 12.386 6.94 18.718 10.537.206-.433.505-.95.706-1.502a108.66 108.66 0 0 1 32.901-46.762 37.758 37.758 0 0 1 11.822-6.883 17.246 17.246 0 0 1 3.679-.845c6.264-.717 8.893 3.224 9.356 7.932a29.944 29.944 0 0 1-.774 10.354 87.906 87.906 0 0 1-10.73 24.688c-6.79 10.972-14.85 20.855-25.093 28.83-.302.236-.568.519-1.08.99a177.78 177.78 0 0 1 26.593 30.883 10.962 10.962 0 0 1-1.689.298c-10.595.015-21.19-.019-31.786.046a4.004 4.004 0 0 1-3.172-1.69 147.875 147.875 0 0 0-88.178-46.549 143.359 143.359 0 0 0-30.28-1.169 146.407 146.407 0 0 0-82.537 31.81 140.067 140.067 0 0 0-16.976 15.843 4.728 4.728 0 0 1-3.863 1.757c-10.121-.07-20.242-.035-30.363-.035h-2.152c.618-2.408 6.84-10.938 13.883-18.553 5.252-5.679 10.817-11.07 16.468-16.818ZM394.53 347.912a176.639 176.639 0 0 1-23.974 27.164l1.862 1.55a108.315 108.315 0 0 1 33.683 48.146 34.618 34.618 0 0 1 2.202 14.42 14.885 14.885 0 0 1-.748 3.692 7.208 7.208 0 0 1-8.157 5.023 22.233 22.233 0 0 1-6.763-2.006 51.232 51.232 0 0 1-9.182-5.815 107.592 107.592 0 0 1-32.936-46.707c-.187-.514-.392-1.02-.722-1.877a194.65 194.65 0 0 1-25.012 14.008 181.67 181.67 0 0 1-26.687 9.724 187.556 187.556 0 0 1-28.305 5.388c.168.84.265 1.51.438 2.16a109.172 109.172 0 0 1 2.97 36.442 80.804 80.804 0 0 1-4.422 22.478 78.25 78.25 0 0 1-4.165 8.744 13.39 13.39 0 0 1-2.339 2.971c-3.98 4.11-8.732 4.144-12.611-.074a27.28 27.28 0 0 1-3.907-5.617c-3.077-5.776-4.66-12.056-5.791-18.46a116.863 116.863 0 0 1-1.36-26.465 94.48 94.48 0 0 1 2.885-19.186c.14-.532.268-1.07.372-1.61.026-.137-.064-.297-.171-.738a176.121 176.121 0 0 1-80.969-24.994c-.41.91-.762 1.675-1.101 2.446a110.477 110.477 0 0 1-30.901 41.42 38.16 38.16 0 0 1-12.047 6.96 12.09 12.09 0 0 1-6.516.7 7.119 7.119 0 0 1-5.403-4.49c-1.416-3.424-1.165-6.985-.684-10.517a55.453 55.453 0 0 1 4.307-14.25 112.5 112.5 0 0 1 26.512-37.763c.459-.435.93-.857 1.38-1.3a3.76 3.76 0 0 0 .366-.655 178.905 178.905 0 0 1-28.47-31.317c.985-.08 1.644-.18 2.303-.18 10.514-.01 21.029.027 31.543-.044a4.706 4.706 0 0 1 3.703 1.626 146.946 146.946 0 0 0 39.403 28.885 139.947 139.947 0 0 0 49.704 14.774q70.68 6.87 121.6-42.854a7.646 7.646 0 0 1 5.992-2.444c9.802.121 19.605.05 29.408.05h2.534ZM350.736 197.762c2.787 0 5.47.189 8.115-.05 2.995-.271 5.139.8 7.323 2.813 12.613 11.622 25.357 23.1 38.059 34.627.638.58 1.29 1.144 2.11 1.87.764-.657 1.481-1.243 2.165-1.865q19.638-17.878 39.248-35.787a5.448 5.448 0 0 1 4.204-1.646c3.218.13 6.446.038 9.84.038V303.13c-1.722.504-24.875.604-27.638.061V249.83l-.537-.254-27.238 24.841-27.458-24.736-.524.192c-.023 4.454-.008 8.908-.01 13.362q-.005 6.64-.001 13.28v26.871h-27.428c-.514-1.773-.753-99.662-.23-105.623ZM97.634 197.882h27.264c.55 1.753.658 102.972.094 105.525H97.705c-.15-6.703-.048-13.384-.067-20.061-.018-6.623-.004-13.245-.004-20.04H63.847v39.741c-2.06.615-25.334.674-27.648.123V197.894h27.538v37.19c1.968.568 30.924.673 33.872.129.009-2.978.02-6.027.024-9.076q.007-4.744.001-9.487v-18.768ZM157.576 303.368V198.195c1.617-.53 61.545-.736 65.462-.205v22.414c-.879.063-1.786.184-2.693.185q-16.008.02-32.017.009h-2.968v17.433h33.347v23.192h-33.049c-.553 1.985-.705 15.817-.256 19.646.845.057 1.75.17 2.655.17q16.01.019 32.018.009h2.97v22.32ZM254.283 303.409c-.5-2.823-.4-103.602.097-105.518h27.162v77.765c1.172.06 2.092.149 3.011.15q16.128.014 32.256.006h2.908v27.597Z"/></svg>
<!-- RESOURCE DEPLOYMENT ICON -->
<svg id="deployment" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M24.01 20.027v2h-24v-2h4v-1a2.006 2.006 0 0 1-2-2v-10a2.006 2.006 0 0 1 2-2h1.996v2H4.01v10h16v-10h-2.004v-2h2.004a2.006 2.006 0 0 1 2 2l-.01 10a1.997 1.997 0 0 1-1.99 2v1Zm-9-6.012-3-3-3 3h2v2.01h2v-2.01Zm.995-7.991a4 4 0 1 1-4-4 4.001 4.001 0 0 1 4 4Zm-4.4 2.96v-.56a.802.802 0 0 1-.8-.8v-.4L9.06 5.479a2.958 2.958 0 0 0 2.545 3.505Zm2.658-1.007a2.977 2.977 0 0 0-1.068-4.704.797.797 0 0 1-.79.75h-.8v.8a.401.401 0 0 1-.4.4h-.8v.8h2.4a.401.401 0 0 1 .4.4v1.2h.4a.787.787 0 0 1 .658.354Z" fill="#fff"/></svg>
<!-- NODE ICON -->
<svg id="node" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="#fff"><circle cx="12" cy="3" r="1"/><circle cx="20" cy="8" r="1"/><circle cx="20" cy="16" r="1"/><circle cx="4" cy="8" r="1"/><circle cx="4" cy="16" r="1"/><path d="M20 14v-4a1.992 1.992 0 0 1-1.481-3.333l-4.783-2.69a1.983 1.983 0 0 1-3.472 0l-4.783 2.69A1.992 1.992 0 0 1 4 10v4a1.992 1.992 0 0 1 1.481 3.333l4.783 2.69a1.991 1.991 0 0 1 1.236-.952v-5.142a2 2 0 1 1 1 0v5.142a1.991 1.991 0 0 1 1.236.953l4.783-2.69A1.992 1.992 0 0 1 20 14Z"/><circle cx="12" cy="21" r="1"/><circle cx="12" cy="12" r="1"/></g></svg>
<!-- RESOURCE OTHER ICON -->
<svg id="other" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#FFF"><path d="M27.476 10.22H14.83l-1.558-3.115a1.327 1.327 0 0 0-1.184-.732H4.522c-.731 0-1.324.593-1.324 1.324v16.606c0 .731.593 1.324 1.324 1.324h22.953c.731 0 1.324-.593 1.324-1.324v-12.76c0-.731-.593-1.324-1.324-1.324z"/></svg>
</defs>
<use id="customIcon" href="#${ type }" fill="#fff" />`;

View File

@ -2,7 +2,7 @@ import { DSL } from '@shell/store/type-map';
import { FLEET } from '@shell/config/types';
import { STATE, NAME as NAME_COL, AGE, FLEET_APPLICATION_TYPE } from '@shell/config/table-headers';
import { FLEET as FLEET_FEATURE } from '@shell/store/features';
import { gitRepoGraphConfig } from '@shell/pages/c/_cluster/fleet/GitRepoGraphConfig';
import { graphConfig } from '@shell/pages/c/_cluster/fleet/graph/config';
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
export const SOURCE_TYPE = {
@ -142,9 +142,11 @@ export function init(store) {
], 'resources');
configureType(FLEET.GIT_REPO, {
showListMasthead: false, hasGraph: true, graphConfig: gitRepoGraphConfig
showListMasthead: false, hasGraph: true, graphConfig
});
configureType(FLEET.HELM_OP, {
showListMasthead: false, hasGraph: true, graphConfig
});
configureType(FLEET.HELM_OP, { showListMasthead: false, hasGraph: false });
weightType(FLEET.GIT_REPO, 110, true);
weightType(FLEET.HELM_OP, 109, true);

View File

@ -1,249 +0,0 @@
import { STATES } from '@shell/plugins/dashboard-store/resource-class';
import { FLEET } from '@shell/config/types';
// some default values
const defaultNodeRadius = 20;
const defaultNodePadding = 15;
const chartWidth = 800;
const chartHeight = 500;
const fdcStrength = -300;
const fdcDistanceMax = 500;
const fdcForceCollide = 80;
const fdcAlphaDecay = 0.05;
// setting up default sim params
// check documentation here: https://github.com/d3/d3-force#forceSimulation
const simulationParams = {
fdcStrength,
fdcDistanceMax,
fdcForceCollide,
fdcAlphaDecay
};
/**
* Represents a config object for FDC type
* @param {Function} parseData - Parses the specific data for each chart. Format must be compliant with d3 data format
* @example data format => { parent: {..., children: [ {..., children: []} ] } }
* @param {Function} extendNodeClass - Extends the classes for each node so that the styling is correctly applied
* @param {Function} nodeDimensions - Sets the radius of the nodes according each data type
* @param {Function} infoDetails - Prepares the data to be displayed in the info box on the right-side of the ForceDirectedTreeChart component
*/
export const gitRepoGraphConfig = {
chartWidth,
chartHeight,
simulationParams,
/**
* data prop that is used to trigger the watcher in the component. Should follow format "data.xxxxxx"
*/
watcherProp: 'data.bundles',
/**
* Mandatory params for a child object in parseData (for statuses to work)
* @param {String} state
* @param {String} stateDisplay
* @param {String} stateColor
* @param {String} matchingId (this can be different than the actual ID, depends on the usecase)
*/
parseData: (data) => {
const bundles = data.bundles.map((bundle, i) => {
const bundleLowercaseState = bundle.state ? bundle.state.toLowerCase() : 'unknown';
const bundleStateColor = STATES[bundleLowercaseState].color;
const repoChild = {
id: bundle.id,
matchingId: bundle.id,
type: bundle.type,
state: bundle.state,
stateLabel: bundle.stateDisplay,
stateColor: bundleStateColor,
isBundle: true,
errorMsg: bundle.stateDescription,
detailLocation: bundle.detailLocation,
children: []
};
const bds = data.bundleDeployments.filter((bd) => bundle.id === `${ bd.metadata?.labels?.['fleet.cattle.io/bundle-namespace'] }/${ bd.metadata?.labels?.['fleet.cattle.io/bundle-name'] }`);
bds.forEach((bd) => {
const bdLowercaseState = bd.state ? bd.state.toLowerCase() : 'unknown';
const bdStateColor = STATES[bdLowercaseState]?.color;
const cluster = data.clustersList.find((cluster) => {
const clusterString = `${ cluster.namespace }-${ cluster.name }`;
return bd.id.includes(clusterString);
});
repoChild.children.push({
id: bd.id,
matchingId: bd.id,
type: bd.type,
clusterLabel: cluster ? cluster.namespacedName : undefined,
clusterDetailLocation: cluster ? cluster.detailLocation : undefined,
state: bd.state,
stateLabel: bd.stateDisplay,
stateColor: bdStateColor,
isBundleDeployment: true,
errorMsg: bd.stateDescription,
detailLocation: bd.detailLocation,
});
});
return repoChild;
});
const repoLowercaseState = data.state ? data.state.toLowerCase() : 'unknown';
const repoStateColor = STATES[repoLowercaseState].color;
const finalData = {
id: data.id,
matchingId: data.id,
type: data.type,
state: data.state,
stateLabel: data.stateDisplay,
stateColor: repoStateColor,
isRepo: true,
errorMsg: data.stateDescription,
detailLocation: data.detailLocation,
children: bundles
};
return finalData;
},
/**
* Used to add relevant classes to each main node instance
*/
extendNodeClass: ({ data }) => {
const classArray = [];
// node type
data?.isRepo ? classArray.push('repo') : data?.isBundle ? classArray.push('bundle') : classArray.push('bundle-deployment');
return classArray;
},
/**
* Used to add the correct icon to each node
*/
fetchNodeIcon: ({ data }) => {
if (data?.isRepo) {
return 'git';
}
if ( data?.isBundle) {
if (data?.id.indexOf('helm') !== -1) {
return 'helm';
}
return 'bundle';
}
if (data?.isBundleDeployment) {
return 'node';
}
},
/**
* Used to set node dimensions
*/
nodeDimensions: ({ data }) => {
if (data?.isRepo) {
const radius = defaultNodeRadius * 3;
const padding = defaultNodePadding * 2.5;
return {
radius,
size: (radius * 2) - padding,
position: -(((radius * 2) - padding) / 2)
};
}
if (data?.isBundle) {
const radius = defaultNodeRadius * 2;
const padding = defaultNodePadding;
if (data?.id.indexOf('helm') !== -1) {
return {
radius,
size: (radius * 1.5) - padding,
position: -(((radius * 1.5) - padding) / 2)
};
}
return {
radius,
size: (radius * 1.7) - padding,
position: -(((radius * 1.7) - padding) / 2)
};
}
return {
radius: defaultNodeRadius,
size: (defaultNodeRadius * 2) - defaultNodePadding,
position: -(((defaultNodeRadius * 2) - defaultNodePadding) / 2)
};
},
/**
* Use @param {Obj} valueObj for compound values (usually associated with a template of some sort on the actual component)
* or @param value for a simple straightforward value
*/
infoDetails: (data) => {
let dataType;
switch (data.type) {
case FLEET.GIT_REPO:
dataType = 'GitRepo';
break;
case FLEET.BUNDLE:
dataType = 'Bundle';
break;
case FLEET.BUNDLE_DEPLOYMENT:
dataType = 'BundleDeployment';
break;
default:
dataType = data.type;
break;
}
const moreInfo = [
{
labelKey: 'fleet.fdc.type',
value: dataType
},
{
type: 'title-link',
labelKey: 'fleet.fdc.id',
valueObj: {
label: data.id,
detailLocation: data.detailLocation
}
}
];
if (data.isBundleDeployment) {
moreInfo.push({
type: 'title-link',
labelKey: 'fleet.fdc.cluster',
valueObj: {
label: data.clusterLabel,
detailLocation: data.clusterDetailLocation
}
});
}
moreInfo.push({
type: 'state-badge',
labelKey: 'fleet.fdc.state',
valueObj: {
stateColor: data.stateColor,
stateLabel: data.stateLabel
}
});
if (data.errorMsg) {
moreInfo.push({
type: 'single-error',
labelKey: 'fleet.fdc.error',
value: data.errorMsg
});
}
return moreInfo;
}
};

View File

@ -0,0 +1,277 @@
import { STATES } from '@shell/plugins/dashboard-store/resource-class';
import { FLEET } from '@shell/config/types';
import { checkSchemasForFindAllHash } from '@shell/utils/auth';
// TODO use Rancher icons
const chartIcon = (type) => `<defs>
<!-- GIT REPO ICON -->
<svg id="git" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" style="enable-background:new 0 0 96 96" xml:space="preserve"><path class="st0" d="M92.138 43.888 52.018 3.77a5.918 5.918 0 0 0-8.369 0l-8.33 8.332L45.887 22.67a7.025 7.025 0 0 1 7.23 1.684 7.031 7.031 0 0 1 1.67 7.275l10.185 10.185a7.03 7.03 0 0 1 7.275 1.67 7.04 7.04 0 0 1 0 9.958 7.042 7.042 0 0 1-11.492-7.658l-9.5-9.499v24.997a7.09 7.09 0 0 1 1.861 1.331 7.042 7.042 0 1 1-7.65-1.537V35.849a7.04 7.04 0 0 1-3.822-9.234l-10.418-10.42-27.51 27.508a5.921 5.921 0 0 0 0 8.371l40.121 40.118a5.919 5.919 0 0 0 8.37 0l39.93-39.932a5.92 5.92 0 0 0 0-8.37z"/></svg>
<!-- GENERIC BUNDLE ICON -->
<svg id="bundle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#FFF"><path d="M16 3.2C8.931 3.2 3.2 8.931 3.2 16S8.931 28.8 16 28.8 28.8 23.069 28.8 16 23.069 3.2 16 3.2zm0 22.4c-5.302 0-9.6-4.298-9.6-9.6s4.298-9.6 9.6-9.6 9.6 4.298 9.6 9.6a9.6 9.6 0 0 1-9.6 9.6z"/><path d="m24.086 16-6.232-1.348.917-1.424-1.424.917-1.348-6.232-1.348 6.232-1.424-.917.917 1.424L7.912 16l6.232 1.348-.917 1.424 1.424-.917 1.348 6.232 1.348-6.232 1.424.917-.917-1.424L24.086 16zM16 16.814a.814.814 0 1 1 0-1.628.814.814 0 0 1 0 1.628z"/></svg>
<!-- HELM BUNDLE ICON -->
<svg id="helm" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><defs><style>.cls-1{fill:#fff}</style></defs><path class="cls-1" d="M136.53 121.135c-.573-.543-1.195-1.127-1.81-1.718-12.617-12.121-22.382-26.136-28.28-42.702-1.65-4.636-2.897-9.365-2.675-14.359.02-.473.02-.949.052-1.422.445-6.446 4.759-9.703 11.058-8.176a27.325 27.325 0 0 1 5.732 2.196c6.89 3.456 12.506 8.564 17.67 14.166A112.527 112.527 0 0 1 160 102.55a8.296 8.296 0 0 0 .39.86c.07.13.222.213.559.52a176.357 176.357 0 0 1 81.02-24.861c-.177-.876-.282-1.546-.448-2.2a112.494 112.494 0 0 1-2.653-36.957 84.075 84.075 0 0 1 4.445-21.764 31.326 31.326 0 0 1 5.476-10.17 15.687 15.687 0 0 1 3.164-2.822 7.026 7.026 0 0 1 8.032-.056 17.279 17.279 0 0 1 5.84 6.731 53.054 53.054 0 0 1 5.263 14.677 112.505 112.505 0 0 1 2.122 33.004 95.598 95.598 0 0 1-3.49 19.91c7.121 1.312 14.21 2.33 21.147 3.978a186.38 186.38 0 0 1 20.44 6.003 188.317 188.317 0 0 1 19.77 8.57c6.346 3.163 12.386 6.94 18.718 10.537.206-.433.505-.95.706-1.502a108.66 108.66 0 0 1 32.901-46.762 37.758 37.758 0 0 1 11.822-6.883 17.246 17.246 0 0 1 3.679-.845c6.264-.717 8.893 3.224 9.356 7.932a29.944 29.944 0 0 1-.774 10.354 87.906 87.906 0 0 1-10.73 24.688c-6.79 10.972-14.85 20.855-25.093 28.83-.302.236-.568.519-1.08.99a177.78 177.78 0 0 1 26.593 30.883 10.962 10.962 0 0 1-1.689.298c-10.595.015-21.19-.019-31.786.046a4.004 4.004 0 0 1-3.172-1.69 147.875 147.875 0 0 0-88.178-46.549 143.359 143.359 0 0 0-30.28-1.169 146.407 146.407 0 0 0-82.537 31.81 140.067 140.067 0 0 0-16.976 15.843 4.728 4.728 0 0 1-3.863 1.757c-10.121-.07-20.242-.035-30.363-.035h-2.152c.618-2.408 6.84-10.938 13.883-18.553 5.252-5.679 10.817-11.07 16.468-16.818ZM394.53 347.912a176.639 176.639 0 0 1-23.974 27.164l1.862 1.55a108.315 108.315 0 0 1 33.683 48.146 34.618 34.618 0 0 1 2.202 14.42 14.885 14.885 0 0 1-.748 3.692 7.208 7.208 0 0 1-8.157 5.023 22.233 22.233 0 0 1-6.763-2.006 51.232 51.232 0 0 1-9.182-5.815 107.592 107.592 0 0 1-32.936-46.707c-.187-.514-.392-1.02-.722-1.877a194.65 194.65 0 0 1-25.012 14.008 181.67 181.67 0 0 1-26.687 9.724 187.556 187.556 0 0 1-28.305 5.388c.168.84.265 1.51.438 2.16a109.172 109.172 0 0 1 2.97 36.442 80.804 80.804 0 0 1-4.422 22.478 78.25 78.25 0 0 1-4.165 8.744 13.39 13.39 0 0 1-2.339 2.971c-3.98 4.11-8.732 4.144-12.611-.074a27.28 27.28 0 0 1-3.907-5.617c-3.077-5.776-4.66-12.056-5.791-18.46a116.863 116.863 0 0 1-1.36-26.465 94.48 94.48 0 0 1 2.885-19.186c.14-.532.268-1.07.372-1.61.026-.137-.064-.297-.171-.738a176.121 176.121 0 0 1-80.969-24.994c-.41.91-.762 1.675-1.101 2.446a110.477 110.477 0 0 1-30.901 41.42 38.16 38.16 0 0 1-12.047 6.96 12.09 12.09 0 0 1-6.516.7 7.119 7.119 0 0 1-5.403-4.49c-1.416-3.424-1.165-6.985-.684-10.517a55.453 55.453 0 0 1 4.307-14.25 112.5 112.5 0 0 1 26.512-37.763c.459-.435.93-.857 1.38-1.3a3.76 3.76 0 0 0 .366-.655 178.905 178.905 0 0 1-28.47-31.317c.985-.08 1.644-.18 2.303-.18 10.514-.01 21.029.027 31.543-.044a4.706 4.706 0 0 1 3.703 1.626 146.946 146.946 0 0 0 39.403 28.885 139.947 139.947 0 0 0 49.704 14.774q70.68 6.87 121.6-42.854a7.646 7.646 0 0 1 5.992-2.444c9.802.121 19.605.05 29.408.05h2.534ZM350.736 197.762c2.787 0 5.47.189 8.115-.05 2.995-.271 5.139.8 7.323 2.813 12.613 11.622 25.357 23.1 38.059 34.627.638.58 1.29 1.144 2.11 1.87.764-.657 1.481-1.243 2.165-1.865q19.638-17.878 39.248-35.787a5.448 5.448 0 0 1 4.204-1.646c3.218.13 6.446.038 9.84.038V303.13c-1.722.504-24.875.604-27.638.061V249.83l-.537-.254-27.238 24.841-27.458-24.736-.524.192c-.023 4.454-.008 8.908-.01 13.362q-.005 6.64-.001 13.28v26.871h-27.428c-.514-1.773-.753-99.662-.23-105.623ZM97.634 197.882h27.264c.55 1.753.658 102.972.094 105.525H97.705c-.15-6.703-.048-13.384-.067-20.061-.018-6.623-.004-13.245-.004-20.04H63.847v39.741c-2.06.615-25.334.674-27.648.123V197.894h27.538v37.19c1.968.568 30.924.673 33.872.129.009-2.978.02-6.027.024-9.076q.007-4.744.001-9.487v-18.768ZM157.576 303.368V198.195c1.617-.53 61.545-.736 65.462-.205v22.414c-.879.063-1.786.184-2.693.185q-16.008.02-32.017.009h-2.968v17.433h33.347v23.192h-33.049c-.553 1.985-.705 15.817-.256 19.646.845.057 1.75.17 2.655.17q16.01.019 32.018.009h2.97v22.32ZM254.283 303.409c-.5-2.823-.4-103.602.097-105.518h27.162v77.765c1.172.06 2.092.149 3.011.15q16.128.014 32.256.006h2.908v27.597Z"/></svg>
<!-- RESOURCE DEPLOYMENT ICON -->
<svg id="deployment" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M24.01 20.027v2h-24v-2h4v-1a2.006 2.006 0 0 1-2-2v-10a2.006 2.006 0 0 1 2-2h1.996v2H4.01v10h16v-10h-2.004v-2h2.004a2.006 2.006 0 0 1 2 2l-.01 10a1.997 1.997 0 0 1-1.99 2v1Zm-9-6.012-3-3-3 3h2v2.01h2v-2.01Zm.995-7.991a4 4 0 1 1-4-4 4.001 4.001 0 0 1 4 4Zm-4.4 2.96v-.56a.802.802 0 0 1-.8-.8v-.4L9.06 5.479a2.958 2.958 0 0 0 2.545 3.505Zm2.658-1.007a2.977 2.977 0 0 0-1.068-4.704.797.797 0 0 1-.79.75h-.8v.8a.401.401 0 0 1-.4.4h-.8v.8h2.4a.401.401 0 0 1 .4.4v1.2h.4a.787.787 0 0 1 .658.354Z" fill="#fff"/></svg>
<!-- NODE ICON -->
<svg id="node" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="#fff"><circle cx="12" cy="3" r="1"/><circle cx="20" cy="8" r="1"/><circle cx="20" cy="16" r="1"/><circle cx="4" cy="8" r="1"/><circle cx="4" cy="16" r="1"/><path d="M20 14v-4a1.992 1.992 0 0 1-1.481-3.333l-4.783-2.69a1.983 1.983 0 0 1-3.472 0l-4.783 2.69A1.992 1.992 0 0 1 4 10v4a1.992 1.992 0 0 1 1.481 3.333l4.783 2.69a1.991 1.991 0 0 1 1.236-.952v-5.142a2 2 0 1 1 1 0v5.142a1.991 1.991 0 0 1 1.236.953l4.783-2.69A1.992 1.992 0 0 1 20 14Z"/><circle cx="12" cy="21" r="1"/><circle cx="12" cy="12" r="1"/></g></svg>
<!-- RESOURCE OTHER ICON -->
<svg id="other" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#FFF"><path d="M27.476 10.22H14.83l-1.558-3.115a1.327 1.327 0 0 0-1.184-.732H4.522c-.731 0-1.324.593-1.324 1.324v16.606c0 .731.593 1.324 1.324 1.324h22.953c.731 0 1.324-.593 1.324-1.324v-12.76c0-.731-.593-1.324-1.324-1.324z"/></svg>
</defs>
<use id="customIcon" href="#${ type }" fill="#fff" />`;
// some default values
const defaultNodeRadius = 20;
const defaultNodePadding = 15;
const chartWidth = 800;
const chartHeight = 500;
const fdcStrength = -300;
const fdcDistanceMax = 500;
const fdcForceCollide = 80;
const fdcAlphaDecay = 0.05;
// setting up default sim params
// check documentation here: https://github.com/d3/d3-force#forceSimulation
const simulationParams = {
fdcStrength,
fdcDistanceMax,
fdcForceCollide,
fdcAlphaDecay
};
/**
* Represents a config object for FDC type
* @param {Function} parseData - Parses the specific data for each chart. Format must be compliant with d3 data format
* @example data format => { parent: {..., children: [ {..., children: []} ] } }
* @param {Function} extendNodeClass - Extends the classes for each node so that the styling is correctly applied
* @param {Function} nodeDimensions - Sets the radius of the nodes according each data type
* @param {Function} infoDetails - Prepares the data to be displayed in the info box on the right-side of the ForceDirectedTreeChart component
*/
export const graphConfig = {
chartWidth,
chartHeight,
simulationParams,
/**
* data prop that is used to trigger the watcher in the component. Should follow format "data.xxxxxx"
*/
watcherProp: 'data.bundles',
/**
* Mandatory params for a child object in parseData (for statuses to work)
* @param {String} state
* @param {String} stateDisplay
* @param {String} stateColor
* @param {String} matchingId (this can be different than the actual ID, depends on the usecase)
*/
parseData: (data) => {
const bundles = data.bundles.map((bundle) => {
const bundleLowercaseState = bundle.state ? bundle.state.toLowerCase() : 'unknown';
const bundleStateColor = STATES[bundleLowercaseState].color;
const appChild = {
id: bundle.id,
type: bundle.type,
matchingId: `${ bundle.type }-${ bundle.id }`,
state: bundle.state,
stateLabel: bundle.stateDisplay,
stateColor: bundleStateColor,
errorMsg: bundle.stateDescription,
detailLocation: bundle.detailLocation,
children: []
};
const bds = data.bundleDeployments.filter((bd) => bundle.id === `${ bd.metadata?.labels?.['fleet.cattle.io/bundle-namespace'] }/${ bd.metadata?.labels?.['fleet.cattle.io/bundle-name'] }`);
bds.forEach((bd) => {
const bdLowercaseState = bd.state ? bd.state.toLowerCase() : 'unknown';
const bdStateColor = STATES[bdLowercaseState]?.color;
const cluster = data.clustersList.find((cluster) => {
const clusterString = `${ cluster.namespace }-${ cluster.name }`;
return bd.id.includes(clusterString);
});
appChild.children.push({
id: bd.id,
type: bd.type,
matchingId: `${ bd.type }-${ bd.id }`,
clusterLabel: cluster ? cluster.namespacedName : undefined,
clusterDetailLocation: cluster ? cluster.detailLocation : undefined,
state: bd.state,
stateLabel: bd.stateDisplay,
stateColor: bdStateColor,
errorMsg: bd.stateDescription,
detailLocation: bd.detailLocation,
});
});
return appChild;
});
const appLowercaseState = data.state ? data.state.toLowerCase() : 'unknown';
const appStateColor = STATES[appLowercaseState].color;
return {
id: data.id,
type: data.type,
matchingId: `${ data.type }-${ data.id }`,
state: data.state,
stateLabel: data.stateDisplay,
stateColor: appStateColor,
errorMsg: data.stateDescription,
detailLocation: data.detailLocation,
children: bundles,
muteStatus: true
};
},
/**
* Used to add relevant classes to each main node instance
*/
extendNodeClass: ({ data }) => {
const classArray = [];
if (data?.type) {
const nodeType = data.type.replaceAll('fleet.cattle.io.', '');
classArray.push(nodeType);
}
return classArray;
},
/**
* Used to add the correct icon to each node
*/
fetchNodeIcon: ({ data }) => {
let type = '';
switch (data?.type) {
case FLEET.GIT_REPO:
type = 'git';
break;
case FLEET.HELM_OP:
type = 'helm';
break;
case FLEET.BUNDLE:
if (data?.id.indexOf('helm') !== -1) {
type = 'helm';
}
type = 'bundle';
break;
case FLEET.BUNDLE_DEPLOYMENT:
type = 'node';
break;
}
return chartIcon(type);
},
/**
* Used to set node dimensions
*/
nodeDimensions: ({ data }) => {
if (data?.type === FLEET.GIT_REPO || data?.type === FLEET.HELM_OP) {
const radius = defaultNodeRadius * 3;
const padding = defaultNodePadding * 2.5;
return {
radius,
size: (radius * 2) - padding,
position: -(((radius * 2) - padding) / 2)
};
}
if (data?.type === FLEET.BUNDLE) {
const radius = defaultNodeRadius * 2;
const padding = defaultNodePadding;
if (data?.id.indexOf('helm') !== -1) {
return {
radius,
size: (radius * 1.5) - padding,
position: -(((radius * 1.5) - padding) / 2)
};
}
return {
radius,
size: (radius * 1.7) - padding,
position: -(((radius * 1.7) - padding) / 2)
};
}
return {
radius: defaultNodeRadius,
size: (defaultNodeRadius * 2) - defaultNodePadding,
position: -(((defaultNodeRadius * 2) - defaultNodePadding) / 2)
};
},
/**
* Use @param {Obj} valueObj for compound values (usually associated with a template of some sort on the actual component)
* or @param value for a simple straightforward value
*/
infoDetails: (data) => {
const moreInfo = [
{
type: 'resource-type',
labelKey: 'fleet.fdc.type',
valueKey: data.type
},
{
type: 'title-link',
labelKey: 'fleet.fdc.id',
valueObj: {
label: data.id,
detailLocation: data.detailLocation
}
}
];
if (data?.type === FLEET.BUNDLE_DEPLOYMENT) {
moreInfo.push({
type: 'title-link',
labelKey: 'fleet.fdc.cluster',
valueObj: {
label: data.clusterLabel,
detailLocation: data.clusterDetailLocation
}
});
}
moreInfo.push({
type: 'state-badge',
labelKey: 'fleet.fdc.state',
valueObj: {
stateColor: data.stateColor,
stateLabel: data.stateLabel
}
});
if (data.errorMsg) {
moreInfo.push({
type: 'single-error',
labelKey: 'fleet.fdc.error',
value: data.errorMsg
});
}
return moreInfo;
},
checkSchemaPermissions: async(store) => {
const schemas = await checkSchemasForFindAllHash({
cluster: {
inStoreType: 'management',
type: FLEET.CLUSTER
},
bundle: {
inStoreType: 'management',
type: FLEET.BUNDLE,
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
},
bundleDeployment: {
inStoreType: 'management',
type: FLEET.BUNDLE_DEPLOYMENT
}
}, store);
return schemas.cluster && schemas.bundle && schemas.bundleDeployment;
}
};

View File

@ -20,6 +20,11 @@ import FleetApplications from '@shell/components/fleet/FleetApplications.vue';
import FleetUtils from '@shell/utils/fleet';
import Preset from '@shell/mixins/preset';
const VIEW_MODE = {
TABLE: 'flat',
CARDS: 'cards'
};
export default {
name: 'FleetDashboard',
components: {
@ -101,25 +106,26 @@ export default {
createRoute: { name: 'c-cluster-fleet-application-create' },
permissions: {},
FLEET,
[FLEET.REPO]: [],
[FLEET.HELM_OP]: [],
fleetWorkspaces: [],
viewModeOptions: [
[FLEET.REPO]: [],
[FLEET.HELM_OP]: [],
fleetWorkspaces: [],
VIEW_MODE,
viewModeOptions: [
{
tooltipKey: 'fleet.dashboard.viewMode.table',
icon: 'icon-list-flat',
value: 'flat',
value: VIEW_MODE.TABLE,
},
{
tooltipKey: 'fleet.dashboard.viewMode.cards',
icon: 'icon-apps',
value: 'cards',
value: VIEW_MODE.CARDS,
},
],
CARDS_MIN: 50,
CARDS_SIZE: 50,
cardsCount: {},
viewMode: 'cards',
viewMode: VIEW_MODE.CARDS,
isWorkspaceCollapsed: {},
isStateCollapsed: {},
typeFilter: {},
@ -671,7 +677,7 @@ export default {
</div>
</div>
<div
v-if="viewMode === 'flat'"
v-if="viewMode === VIEW_MODE.TABLE"
class="table-panel"
>
<FleetApplications