mirror of https://github.com/rancher/dashboard.git
526 lines
16 KiB
Vue
526 lines
16 KiB
Vue
<script>
|
|
import { mapState } from 'vuex';
|
|
import { FLEET } from '@shell/config/types';
|
|
import { WORKSPACE } from '@shell/store/prefs';
|
|
import { STATES_ENUM, STATES, getStateLabel } from '@shell/plugins/dashboard-store/resource-class';
|
|
import Loading from '@shell/components/Loading';
|
|
import CollapsibleCard from '@shell/components/CollapsibleCard.vue';
|
|
import ResourceTable from '@shell/components/ResourceTable';
|
|
import CompoundStatusBadge from '@shell/components/CompoundStatusBadge';
|
|
import { checkPermissions, checkSchemasForFindAllHash } from '@shell/utils/auth';
|
|
import { WORKSPACE_ANNOTATION } from '@shell/config/labels-annotations';
|
|
import { filterBy } from '@shell/utils/array';
|
|
import FleetNoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
|
|
import { NAME } from '@shell/config/product/fleet';
|
|
import { xOfy } from '@shell/utils/string';
|
|
|
|
export default {
|
|
name: 'FleetDashboard',
|
|
components: {
|
|
Loading,
|
|
ResourceTable,
|
|
CollapsibleCard,
|
|
CompoundStatusBadge,
|
|
FleetNoWorkspaces
|
|
},
|
|
|
|
async fetch() {
|
|
const hash = await checkSchemasForFindAllHash({
|
|
fleetWorkspaces: {
|
|
inStoreType: 'management',
|
|
type: FLEET.WORKSPACE,
|
|
schemaValidator: (schema) => {
|
|
return !!schema?.links?.collection;
|
|
}
|
|
},
|
|
clusterGroups: {
|
|
inStoreType: 'management',
|
|
type: FLEET.CLUSTER_GROUP
|
|
},
|
|
allBundles: {
|
|
inStoreType: 'management',
|
|
type: FLEET.BUNDLE,
|
|
opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
|
|
},
|
|
gitRepos: {
|
|
inStoreType: 'management',
|
|
type: FLEET.GIT_REPO,
|
|
},
|
|
fleetClusters: {
|
|
inStoreType: 'management',
|
|
type: FLEET.CLUSTER,
|
|
}
|
|
}, this.$store);
|
|
|
|
this.gitRepos = hash.gitRepos;
|
|
this.fleetWorkspacesData = hash.fleetWorkspaces || [];
|
|
|
|
try {
|
|
const permissions = await checkPermissions({ workspaces: { type: FLEET.WORKSPACE }, gitRepos: { type: FLEET.GIT_REPO, schemaValidator: (schema) => schema.resourceMethods.includes('PUT') } }, this.$store.getters);
|
|
|
|
this.permissions = permissions;
|
|
} catch (e) {
|
|
console.error(e); // eslint-disable-line no-console
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
admissableAreas: ['clusters', 'bundles', 'resources'],
|
|
headers: [
|
|
{
|
|
name: 'name',
|
|
labelKey: 'tableHeaders.repoName',
|
|
value: 'nameDisplay',
|
|
sort: ['nameSort'],
|
|
formatter: 'LinkDetail',
|
|
canBeVariable: true,
|
|
},
|
|
{
|
|
name: 'clustersReady',
|
|
labelKey: 'tableHeaders.clustersReady',
|
|
value: 'status.readyClusters',
|
|
sort: 'status.readyClusters',
|
|
search: false,
|
|
},
|
|
{
|
|
name: 'bundlesReady',
|
|
labelKey: 'tableHeaders.bundlesReady',
|
|
value: 'status.readyClusters',
|
|
sort: 'status.readyClusters',
|
|
search: false,
|
|
},
|
|
{
|
|
name: 'resourcesReady',
|
|
labelKey: 'tableHeaders.resourcesReady',
|
|
value: 'status.resourceCounts.ready',
|
|
sort: 'status.resourceCounts.ready',
|
|
}
|
|
],
|
|
schema: {},
|
|
allBundles: [],
|
|
gitRepos: [],
|
|
fleetWorkspacesData: [],
|
|
isCollapsed: {},
|
|
permissions: {},
|
|
getStartedLink: {
|
|
name: 'c-cluster-product-resource-create',
|
|
params: {
|
|
product: NAME,
|
|
resource: FLEET.GIT_REPO
|
|
},
|
|
}
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState(['workspace', 'allNamespaces']),
|
|
fleetWorkspaces() {
|
|
if (this.fleetWorkspacesData?.length) {
|
|
return this.fleetWorkspacesData;
|
|
}
|
|
|
|
// When user doesn't have access to the workspaces fall back to namespaces
|
|
return this.allNamespaces.filter((item) => {
|
|
return item.metadata.annotations[WORKSPACE_ANNOTATION] === WORKSPACE;
|
|
}).map(( obj ) => {
|
|
const repos = filterBy(this.gitRepos, 'metadata.namespace', obj.id);
|
|
|
|
return {
|
|
...obj,
|
|
counts: {
|
|
clusters: '-',
|
|
clusterGroups: '-',
|
|
gitRepos: repos.length
|
|
},
|
|
repos,
|
|
nameDisplay: obj.id
|
|
};
|
|
});
|
|
},
|
|
workspacesData() {
|
|
return this.fleetWorkspaces.filter((ws) => ws.repos && ws.repos.length);
|
|
},
|
|
emptyWorkspaces() {
|
|
return this.fleetWorkspaces.filter((ws) => !ws.repos || !ws.repos.length);
|
|
},
|
|
areAllCardsExpanded() {
|
|
return Object.keys(this.isCollapsed).every((key) => !this.isCollapsed[key]);
|
|
}
|
|
},
|
|
methods: {
|
|
setWorkspaceFilterAndLinkToGitRepo(value) {
|
|
this.$store.commit('updateWorkspace', { value, getters: this.$store.getters } );
|
|
this.$store.dispatch('prefs/set', { key: WORKSPACE, value });
|
|
|
|
this.$router.push({
|
|
name: 'c-cluster-product-resource',
|
|
params: {
|
|
product: NAME,
|
|
resource: FLEET.GIT_REPO
|
|
},
|
|
});
|
|
},
|
|
getStatusInfo(area, row) {
|
|
const defaultStatusInfo = {
|
|
badgeClass: `${ STATES[STATES_ENUM.NOT_READY].color } badge-class-default`,
|
|
icon: STATES[STATES_ENUM.NOT_READY].compoundIcon
|
|
};
|
|
|
|
// classes are defined in the themes SASS files...
|
|
return this.getBadgeClassAndIcon(area, row) || defaultStatusInfo;
|
|
},
|
|
getBadgeClassAndIcon(area, row) {
|
|
let group;
|
|
|
|
if (!this.admissableAreas.includes(area)) {
|
|
return false;
|
|
}
|
|
|
|
if (area === 'clusters') {
|
|
if (row.clusterInfo?.ready === row.clusterInfo?.total && row.clusterInfo?.ready) {
|
|
return {
|
|
badgeClass: STATES[STATES_ENUM.ACTIVE].color,
|
|
icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
|
|
};
|
|
}
|
|
} else if (area === 'bundles') {
|
|
group = row.bundles;
|
|
} else if (area === 'resources') {
|
|
group = row.status?.resources;
|
|
}
|
|
|
|
if (group?.length && group?.every((item) => item.state?.toLowerCase() === STATES_ENUM.ACTIVE)) {
|
|
return {
|
|
badgeClass: STATES[STATES_ENUM.ACTIVE].color ? STATES[STATES_ENUM.ACTIVE].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
|
|
icon: STATES[STATES_ENUM.ACTIVE].compoundIcon ? STATES[STATES_ENUM.ACTIVE].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
|
|
};
|
|
}
|
|
if (group?.length && group?.some((item) => item.state?.toLowerCase() === STATES_ENUM.ERR_APPLIED)) {
|
|
return {
|
|
badgeClass: STATES[STATES_ENUM.ERR_APPLIED].color ? STATES[STATES_ENUM.ERR_APPLIED].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
|
|
icon: STATES[STATES_ENUM.ERR_APPLIED].compoundIcon ? STATES[STATES_ENUM.ERR_APPLIED].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
|
|
};
|
|
}
|
|
if (group?.length && group?.some((item) => item.state?.toLowerCase() === STATES_ENUM.NOT_READY)) {
|
|
return {
|
|
badgeClass: STATES[STATES_ENUM.NOT_READY].color ? STATES[STATES_ENUM.NOT_READY].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
|
|
icon: STATES[STATES_ENUM.NOT_READY].compoundIcon ? STATES[STATES_ENUM.NOT_READY].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
|
|
};
|
|
}
|
|
|
|
if (area === 'resources') {
|
|
if (row.status?.resourceCounts?.desiredReady === row.status?.resourceCounts?.ready && row.status?.resourceCounts?.desiredReady) {
|
|
return {
|
|
badgeClass: STATES[STATES_ENUM.ACTIVE].color,
|
|
icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
badgeClass: `${ STATES[STATES_ENUM.NOT_READY].color } badge-class-area-${ area }`,
|
|
icon: STATES[STATES_ENUM.NOT_READY].compoundIcon
|
|
};
|
|
},
|
|
getTooltipInfo(area, row) {
|
|
let group;
|
|
|
|
if (!this.admissableAreas.includes(area)) {
|
|
return {};
|
|
}
|
|
|
|
if (area === 'clusters') {
|
|
group = '';
|
|
} else if (area === 'bundles') {
|
|
group = row.bundles;
|
|
} else if (area === 'resources') {
|
|
group = row.status?.resources;
|
|
}
|
|
|
|
if (group?.length) {
|
|
return this.generateTooltipData(group);
|
|
}
|
|
|
|
return '';
|
|
},
|
|
generateTooltipData(data) {
|
|
const infoObj = {};
|
|
let tooltipData = '';
|
|
|
|
data.forEach((item) => {
|
|
if (!infoObj[item.state]) {
|
|
infoObj[item.state] = 0;
|
|
}
|
|
|
|
infoObj[item.state]++;
|
|
});
|
|
|
|
Object.keys(infoObj).forEach((key) => {
|
|
tooltipData += `${ getStateLabel(key) }: ${ infoObj[key] }<br>`;
|
|
});
|
|
|
|
return tooltipData;
|
|
},
|
|
getBadgeValue(area, row) {
|
|
let value;
|
|
|
|
if (!this.admissableAreas.includes(area)) {
|
|
return 'N/A';
|
|
}
|
|
|
|
if (area === 'clusters') {
|
|
return `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
|
|
} else if (area === 'bundles') {
|
|
value = xOfy(row.bundlesReady?.length, row.bundles?.length);
|
|
} else if (area === 'resources') {
|
|
value = xOfy(row.status?.resourceCounts?.ready, row.status?.resourceCounts?.desiredReady);
|
|
}
|
|
|
|
return value;
|
|
},
|
|
toggleCollapse(val, key) {
|
|
this.isCollapsed[key] = val;
|
|
},
|
|
toggleAll(action) {
|
|
const val = action !== 'expand';
|
|
|
|
Object.keys(this.isCollapsed).forEach((key) => {
|
|
this.isCollapsed[key] = val;
|
|
});
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
fleetWorkspaces(value) {
|
|
value?.filter((ws) => ws.repos?.length).forEach((ws) => {
|
|
this.isCollapsed[ws.id] = false;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<!-- no git repos -->
|
|
<FleetNoWorkspaces
|
|
v-else-if="!fleetWorkspacesData.length"
|
|
:can-view="permissions.workspaces"
|
|
/>
|
|
<div
|
|
v-else-if="!gitRepos.length"
|
|
class="fleet-empty-dashboard"
|
|
>
|
|
<i class="icon-fleet mb-30" />
|
|
<h1>{{ t('fleet.dashboard.welcome') }}</h1>
|
|
<p class="mb-30">
|
|
<span>{{ t('fleet.dashboard.gitOpsScale') }}</span>
|
|
<a
|
|
:href="t('fleet.dashboard.learnMoreLink')"
|
|
target="_blank"
|
|
rel="noopener noreferrer nofollow"
|
|
>
|
|
{{ t('fleet.dashboard.learnMore') }} <i class="icon icon-external-link" />
|
|
</a>
|
|
</p>
|
|
<template v-if="permissions.gitRepos">
|
|
<h3 class="mb-30">
|
|
{{ t('fleet.dashboard.noRepo', null, true) }}
|
|
</h3>
|
|
<router-link
|
|
:to="getStartedLink"
|
|
class="btn role-secondary"
|
|
>
|
|
{{ t('fleet.dashboard.getStarted') }}
|
|
</router-link>
|
|
</template>
|
|
</div>
|
|
<!-- fleet dashboard with repos -->
|
|
<div
|
|
v-else
|
|
class="fleet-dashboard-data"
|
|
>
|
|
<div class="title">
|
|
<h1>
|
|
<t k="fleet.dashboard.pageTitle" />
|
|
</h1>
|
|
<div>
|
|
<p
|
|
v-if="areAllCardsExpanded"
|
|
@click="toggleAll('collapse')"
|
|
>
|
|
{{ t('fleet.dashboard.collapseAll') }}
|
|
</p>
|
|
<p
|
|
v-else
|
|
@click="toggleAll('expand')"
|
|
>
|
|
{{ t('fleet.dashboard.expandAll') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="emptyWorkspaces.length"
|
|
class="title-footnote"
|
|
>
|
|
<p>{{ t('fleet.dashboard.thereIsMore', { count: emptyWorkspaces.length }) }}: </p>
|
|
<p
|
|
v-for="(ews, i) in emptyWorkspaces"
|
|
:key="i"
|
|
>
|
|
{{ ews.nameDisplay }}<span v-if="i != (emptyWorkspaces.length - 1)">, </span>
|
|
</p>
|
|
</div>
|
|
<CollapsibleCard
|
|
v-for="(ws, i) in workspacesData"
|
|
:key="i"
|
|
class="mt-20 mb-40"
|
|
:title="`${t('resourceDetail.masthead.workspace')}: ${ws.nameDisplay}`"
|
|
:is-collapsed="isCollapsed[ws.id]"
|
|
:is-title-clickable="true"
|
|
:data-testid="`collapsible-card-${ ws.id }`"
|
|
@toggleCollapse="toggleCollapse($event, ws.id)"
|
|
@titleClick="setWorkspaceFilterAndLinkToGitRepo(ws.id)"
|
|
>
|
|
<template v-slot:header-right>
|
|
<div class="header-icons">
|
|
<p>
|
|
<i class="icon icon-repository" />
|
|
<span>{{ t('tableHeaders.repositories') }}: <span>{{ ws.counts.gitRepos }}</span></span>
|
|
</p>
|
|
<p>
|
|
<i class="icon icon-storage" />
|
|
<span>{{ t('tableHeaders.clusters') }}: <span>{{ ws.counts.clusters }}</span></span>
|
|
</p>
|
|
<p>
|
|
<i class="icon icon-folder" />
|
|
<span>{{ t('tableHeaders.clusterGroups') }}: <span>{{ ws.counts.clusterGroups }}</span></span>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
<template v-slot:content>
|
|
<ResourceTable
|
|
:schema="schema"
|
|
:headers="headers"
|
|
:rows="ws.repos"
|
|
key-field="_key"
|
|
:search="false"
|
|
:table-actions="false"
|
|
>
|
|
<template #cell:clustersReady="{row}">
|
|
<span v-if="ws.type === 'namespace'"> - </span>
|
|
<CompoundStatusBadge
|
|
v-else
|
|
data-testid="clusters-ready"
|
|
:tooltip-text="getTooltipInfo('clusters', row)"
|
|
:badge-class="getStatusInfo('clusters', row).badgeClass"
|
|
:icon="getStatusInfo('clusters', row).icon"
|
|
:value="getBadgeValue('clusters', row)"
|
|
/>
|
|
</template>
|
|
<template #cell:bundlesReady="{row}">
|
|
<span v-if="ws.type === 'namespace'"> - </span>
|
|
<CompoundStatusBadge
|
|
v-else
|
|
data-testid="bundles-ready"
|
|
:tooltip-text="getTooltipInfo('bundles', row)"
|
|
:badge-class="getStatusInfo('bundles', row).badgeClass"
|
|
:icon="getStatusInfo('bundles', row).icon"
|
|
:value="getBadgeValue('bundles', row)"
|
|
/>
|
|
</template>
|
|
<template #cell:resourcesReady="{row}">
|
|
<CompoundStatusBadge
|
|
data-testid="resources-ready"
|
|
:tooltip-text="getTooltipInfo('resources', row)"
|
|
:badge-class="getStatusInfo('resources', row).badgeClass"
|
|
:icon="getStatusInfo('resources', row).icon"
|
|
:value="getBadgeValue('resources', row)"
|
|
/>
|
|
</template>
|
|
|
|
<template #cell:target="{row}">
|
|
{{ row.targetInfo.modeDisplay }}
|
|
</template>
|
|
</ResourceTable>
|
|
</template>
|
|
</CollapsibleCard>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.fleet-empty-dashboard {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
min-height: 100%;
|
|
|
|
.icon-fleet {
|
|
font-size: 100px;
|
|
color: var(--disabled-text);
|
|
}
|
|
|
|
> p > span {
|
|
color: var(--disabled-text);
|
|
}
|
|
}
|
|
|
|
.fleet-dashboard-data {
|
|
.title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
min-height: 48px;
|
|
|
|
> div {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
p{
|
|
color: var(--primary);
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.title-footnote {
|
|
display: flex;
|
|
align-items: center;
|
|
color: var(--darker);
|
|
}
|
|
|
|
.header-icons {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
p {
|
|
margin-right: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
> span {
|
|
color: var(--disabled-text);
|
|
|
|
> span {
|
|
color: var(--body-text);
|
|
}
|
|
}
|
|
|
|
i {
|
|
color: var(--disabled-text);
|
|
font-size: 20px;
|
|
margin-right: 10px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|