mirror of https://github.com/rancher/dashboard.git
987 lines
27 KiB
Vue
987 lines
27 KiB
Vue
<script>
|
|
import { getVersionData } from '@shell/config/version';
|
|
import { mapState, mapGetters } from 'vuex';
|
|
import { isEmpty } from '@shell/utils/object';
|
|
import { FLEET } from '@shell/config/types';
|
|
import { WORKSPACE } from '@shell/store/prefs';
|
|
import Loading from '@shell/components/Loading';
|
|
import { checkPermissions, checkSchemasForFindAllHash } from '@shell/utils/auth';
|
|
import { WORKSPACE_ANNOTATION } from '@shell/config/labels-annotations';
|
|
import { filterBy } from '@shell/utils/array';
|
|
import NoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
|
|
import { RcButton } from '@components/RcButton';
|
|
import ResourcePanel from '@shell/components/fleet/dashboard/ResourcePanel.vue';
|
|
import ResourceCard from '@shell/components/fleet/dashboard/ResourceCard.vue';
|
|
import ResourceDetails from '@shell/components/fleet/dashboard/ResourceDetails.vue';
|
|
import EmptyDashboard from '@shell/components/fleet/dashboard/Empty.vue';
|
|
import ButtonGroup from '@shell/components/ButtonGroup';
|
|
import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
|
|
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: {
|
|
ButtonGroup,
|
|
Checkbox,
|
|
EmptyDashboard,
|
|
FleetApplications,
|
|
Loading,
|
|
NoWorkspaces,
|
|
RcButton,
|
|
ResourceCard,
|
|
ResourcePanel,
|
|
},
|
|
|
|
mixins: [Preset],
|
|
|
|
async fetch() {
|
|
const schemas = {
|
|
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,
|
|
},
|
|
helmOps: {
|
|
inStoreType: 'management',
|
|
type: FLEET.HELM_OP,
|
|
},
|
|
fleetClusters: {
|
|
inStoreType: 'management',
|
|
type: FLEET.CLUSTER,
|
|
}
|
|
};
|
|
|
|
const hash = await checkSchemasForFindAllHash(schemas, this.$store);
|
|
|
|
this.fleetWorkspaces = hash.fleetWorkspaces || [];
|
|
|
|
this[FLEET.GIT_REPO] = hash.gitRepos || [];
|
|
this[FLEET.HELM_OP] = hash.helmOps || [];
|
|
|
|
try {
|
|
const permissionsSchemas = {
|
|
workspaces: { type: FLEET.WORKSPACE },
|
|
gitRepos: {
|
|
type: FLEET.GIT_REPO,
|
|
schemaValidator: (schema) => schema.resourceMethods.includes('PUT')
|
|
},
|
|
helmOps: {
|
|
type: FLEET.HELM_OP,
|
|
schemaValidator: (schema) => schema.resourceMethods.includes('PUT')
|
|
},
|
|
};
|
|
|
|
const permissions = await checkPermissions(permissionsSchemas, this.$store.getters);
|
|
|
|
this.permissions = permissions;
|
|
} catch (e) {
|
|
console.error(e); // eslint-disable-line no-console
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
permissions: {},
|
|
FLEET,
|
|
[FLEET.REPO]: [],
|
|
[FLEET.HELM_OP]: [],
|
|
fleetWorkspaces: [],
|
|
VIEW_MODE,
|
|
viewModeOptions: [
|
|
{
|
|
tooltipKey: 'fleet.dashboard.viewMode.table',
|
|
icon: 'icon-list-flat',
|
|
value: VIEW_MODE.TABLE,
|
|
},
|
|
{
|
|
tooltipKey: 'fleet.dashboard.viewMode.cards',
|
|
icon: 'icon-apps',
|
|
value: VIEW_MODE.CARDS,
|
|
},
|
|
],
|
|
CARDS_MIN: 50,
|
|
CARDS_SIZE: 50,
|
|
cardsCount: {},
|
|
viewMode: VIEW_MODE.CARDS,
|
|
isWorkspaceCollapsed: {},
|
|
isStateCollapsed: {},
|
|
typeFilter: {},
|
|
stateFilter: {},
|
|
selectedCard: null,
|
|
presetVersion: getVersionData()?.Version,
|
|
};
|
|
},
|
|
|
|
created() {
|
|
this.$store.dispatch('showWorkspaceSwitcher', false);
|
|
},
|
|
|
|
mounted() {
|
|
this.preset('cardsCount', 'object');
|
|
this.preset('viewMode', 'string');
|
|
},
|
|
|
|
beforeUnmount() {
|
|
this.$store.dispatch('showWorkspaceSwitcher', true);
|
|
},
|
|
|
|
computed: {
|
|
...mapState(['workspace', 'allNamespaces']),
|
|
...mapGetters({ isOpenSlideInPanel: 'slideInPanel/isOpen' }),
|
|
...mapGetters({ isClosingSlideInPanel: 'slideInPanel/isClosing' }),
|
|
|
|
repoSchema() {
|
|
return this.$store.getters['management/schemaFor'](FLEET.GIT_REPO);
|
|
},
|
|
|
|
createRoute() {
|
|
return { name: 'c-cluster-fleet-application-create' };
|
|
},
|
|
|
|
workspaces() {
|
|
if (this.fleetWorkspaces?.length) {
|
|
return this.fleetWorkspaces;
|
|
}
|
|
|
|
// 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[FLEET.GIT_REPO], 'metadata.namespace', obj.id);
|
|
const helmOps = filterBy(this[FLEET.HELM_OP], 'metadata.namespace', obj.id);
|
|
|
|
return {
|
|
...obj,
|
|
repos,
|
|
helmOps,
|
|
id: obj.id
|
|
};
|
|
});
|
|
},
|
|
|
|
applicationStates() {
|
|
return this._groupByWorkspace((ws) => this._resourceStates([...ws.repos, ...ws.helmOps]));
|
|
},
|
|
|
|
clusterStates() {
|
|
return this._groupByWorkspace((ws) => this._resourceStates(ws.clusters));
|
|
},
|
|
|
|
clusterGroupsStates() {
|
|
return this._groupByWorkspace((ws) => this._resourceStates(ws.clusterGroups));
|
|
},
|
|
|
|
cardResources() {
|
|
return this._groupByWorkspace((ws) => {
|
|
const filtered = this.applicationStates[ws.id].reduce((acc, state) => ({
|
|
...acc,
|
|
[state.stateDisplay]: this._filterResources(state),
|
|
}), {});
|
|
|
|
return filtered;
|
|
});
|
|
},
|
|
|
|
tableResources() {
|
|
return this._groupByWorkspace((ws) => {
|
|
const filtered = this.applicationStates[ws.id].reduce((acc, state) => ([
|
|
...acc,
|
|
...this._filterResources(state)
|
|
]), []);
|
|
|
|
return filtered;
|
|
});
|
|
},
|
|
|
|
isEmptyDashboard() {
|
|
return this[FLEET.GIT_REPO]?.length === 0 && this[FLEET.HELM_OP]?.length === 0;
|
|
},
|
|
|
|
allCardsExpanded() {
|
|
return Object.keys(this.isWorkspaceCollapsed).every((key) => !this.isWorkspaceCollapsed[key]);
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
selectStates(workspace, state) {
|
|
this._checkInit(workspace, 'stateFilter');
|
|
|
|
this._cleanStateFilter(workspace);
|
|
|
|
if (this.stateFilter[workspace][state]) {
|
|
delete this.stateFilter[workspace][state];
|
|
} else {
|
|
this.stateFilter[workspace][state] = true;
|
|
}
|
|
|
|
if (this.isWorkspaceCollapsed[workspace]) {
|
|
this.toggleCard(workspace);
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.toggleStateAll(workspace, 'expand');
|
|
});
|
|
},
|
|
|
|
selectType(workspace, type, value) {
|
|
this._checkInit(workspace, 'typeFilter');
|
|
|
|
this.typeFilter[workspace][type] = value;
|
|
|
|
this.toggleStateAll(workspace, 'expand');
|
|
},
|
|
|
|
toggleCard(key) {
|
|
this.isWorkspaceCollapsed[key] = !this.isWorkspaceCollapsed[key];
|
|
},
|
|
|
|
toggleCardAll(action) {
|
|
const val = action !== 'expand';
|
|
|
|
Object.keys(this.isWorkspaceCollapsed).forEach((key) => {
|
|
this.isWorkspaceCollapsed[key] = val;
|
|
});
|
|
},
|
|
|
|
toggleState(workspace, state) {
|
|
this._checkInit(workspace, 'isStateCollapsed');
|
|
|
|
this.isStateCollapsed[workspace][state] = !this.isStateCollapsed[workspace][state];
|
|
},
|
|
|
|
toggleStateAll(workspace, action) {
|
|
const val = action !== 'expand';
|
|
|
|
Object.keys(this.isStateCollapsed[workspace] || []).forEach((state) => {
|
|
this.isStateCollapsed[workspace][state] = val;
|
|
});
|
|
},
|
|
|
|
loadMore(workspace, state) {
|
|
this._checkInit(workspace, 'cardsCount');
|
|
|
|
const count = this.cardsCount[workspace][state] || this.CARDS_MIN;
|
|
|
|
const val = count + this.CARDS_SIZE;
|
|
|
|
this.cardsCount[workspace][state] = val;
|
|
},
|
|
|
|
loadLess(workspace, state) {
|
|
this._checkInit(workspace, 'cardsCount');
|
|
|
|
const count = this.cardsCount[workspace][state] || this.CARDS_MIN;
|
|
|
|
const val = count - this.CARDS_MIN < 0 ? this.CARDS_MIN : count - this.CARDS_SIZE;
|
|
|
|
this.cardsCount[workspace][state] = val;
|
|
},
|
|
|
|
createResource(workspace) {
|
|
this.$store.dispatch('showWorkspaceSwitcher', true);
|
|
|
|
this.$nextTick(() => {
|
|
this.$store.commit('updateWorkspace', { value: workspace, getters: this.$store.getters });
|
|
this.$router.push(this.createRoute);
|
|
});
|
|
},
|
|
|
|
showResourceDetails(value, statePanel, workspace, selected) {
|
|
if (this.isClosingSlideInPanel) {
|
|
return;
|
|
}
|
|
|
|
this.selectedCard = selected;
|
|
|
|
this.$shell.slideInPanel({
|
|
component: ResourceDetails,
|
|
componentProps: {
|
|
value,
|
|
statePanel,
|
|
workspace,
|
|
showHeader: false,
|
|
width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
|
|
zIndex: 1,
|
|
triggerFocusTrap: true,
|
|
returnFocusSelector: `[data-testid="resource-card-${ value.id }"]`
|
|
}
|
|
});
|
|
},
|
|
|
|
_resourceStates(resources) {
|
|
const out = [];
|
|
|
|
resources.forEach((obj) => {
|
|
const {
|
|
stateDisplay,
|
|
stateSort
|
|
} = obj;
|
|
|
|
const exists = out.find((s) => s.stateDisplay === stateDisplay);
|
|
|
|
if (exists) {
|
|
exists.resources.push(obj);
|
|
} else {
|
|
out.push({
|
|
stateDisplay,
|
|
stateSort,
|
|
statePanel: FleetUtils.getDashboardState(obj),
|
|
resources: [obj]
|
|
});
|
|
}
|
|
});
|
|
|
|
return out.sort((a, b) => a.stateSort.localeCompare(b.stateSort));
|
|
},
|
|
|
|
_filterResources(state) {
|
|
return state.resources.filter((item) => this._decodeTypeFilter(item.namespace, item.type) &&
|
|
this._decodeStateFilter(item.namespace, state)
|
|
);
|
|
},
|
|
|
|
_groupByWorkspace(callback) {
|
|
return this.workspaces.reduce((acc, ws) => ({
|
|
...acc,
|
|
[ws.id]: callback(ws)
|
|
}), {});
|
|
},
|
|
|
|
_stateExistsInWorkspace(workspace, state) {
|
|
return !!this.applicationStates[workspace].find((s) => s.statePanel.id === state);
|
|
},
|
|
|
|
_decodeStateFilter(workspace, state) {
|
|
const stateFilter = Object.keys(this.stateFilter[workspace] || {});
|
|
|
|
if (stateFilter.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (stateFilter.filter((key) => this.stateFilter[workspace][key] && this._stateExistsInWorkspace(workspace, key)).length === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (this.stateFilter[workspace][state.statePanel.id]) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
_decodeTypeFilter(workspace, type) {
|
|
const emptyFilter = isEmpty(this.typeFilter) || !this.viewMode;
|
|
|
|
return emptyFilter || this.typeFilter[workspace]?.[type];
|
|
},
|
|
|
|
_cleanStateFilter(workspace) {
|
|
const all = [...Object.keys(this.stateFilter[workspace] || {})];
|
|
|
|
all.forEach((state) => {
|
|
const exists = this._stateExistsInWorkspace(workspace, state);
|
|
|
|
if (!exists) {
|
|
delete this.stateFilter[workspace][state];
|
|
}
|
|
});
|
|
},
|
|
|
|
_checkInit(workspace, name) {
|
|
if (!this[name][workspace]) {
|
|
this[name][workspace] = {};
|
|
}
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
workspaces(neu) {
|
|
if (neu) {
|
|
neu?.forEach((ws) => {
|
|
this.isWorkspaceCollapsed[ws.id] = neu.length > 1;
|
|
|
|
this.isStateCollapsed[ws.id] = { Active: true };
|
|
|
|
this.typeFilter[ws.id] = {
|
|
[FLEET.GIT_REPO]: true,
|
|
[FLEET.HELM_OP]: true,
|
|
};
|
|
|
|
this.stateFilter[ws.id] = {};
|
|
});
|
|
|
|
this.preset('isWorkspaceCollapsed', 'object');
|
|
this.preset('isStateCollapsed', 'object');
|
|
this.preset('typeFilter', 'object');
|
|
this.preset('stateFilter', 'object');
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<NoWorkspaces
|
|
v-else-if="!workspaces?.length"
|
|
:can-view="permissions.workspaces"
|
|
/>
|
|
<EmptyDashboard
|
|
v-else-if="isEmptyDashboard"
|
|
:permissions="permissions"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="dashboard"
|
|
:data-testid="'fleet-dashboard-workspace-cards'"
|
|
>
|
|
<div class="dashboard-header">
|
|
<h1>
|
|
<t k="fleet.dashboard.pageTitle" />
|
|
</h1>
|
|
|
|
<div class="dashboard-main-actions">
|
|
<ButtonGroup
|
|
:data-testid="'view-button'"
|
|
:value="viewMode"
|
|
:options="viewModeOptions"
|
|
@update:value="viewMode = $event"
|
|
/>
|
|
<RcButton
|
|
small
|
|
ghost
|
|
data-testid="fleet-dashboard-expand-all"
|
|
class="collapse-all-btn"
|
|
@click="toggleCardAll(allCardsExpanded ? 'collapse' : 'expand')"
|
|
>
|
|
<p class="ml-10">
|
|
{{ allCardsExpanded ? t('fleet.dashboard.collapseAll') : t('fleet.dashboard.expandAll') }}
|
|
</p>
|
|
<template #after>
|
|
<i
|
|
:class="{
|
|
['icon icon-chevron-down']: !allCardsExpanded,
|
|
['icon icon-chevron-up']: allCardsExpanded,
|
|
}"
|
|
aria-hidden="true"
|
|
/>
|
|
</template>
|
|
</RcButton>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-for="(workspace, i) in workspaces"
|
|
:key="i"
|
|
class="card-container m-0 mt-20"
|
|
:data-testid="`fleet-dashboard-workspace-card-${ workspace.id }`"
|
|
:show-actions="false"
|
|
:show-separator="false"
|
|
:show-highlight-border="false"
|
|
>
|
|
<div class="card-panel-main">
|
|
<div
|
|
class="card-panel-main-details"
|
|
:class="{ expand: !isWorkspaceCollapsed[workspace.id] }"
|
|
>
|
|
<h2 class="workspace-title">
|
|
<span class="workspace-label label-secondary">
|
|
<i class="icon icon-folder" />
|
|
<span>{{ t('fleet.dashboard.workspace') }} : </span>
|
|
</span>
|
|
<router-link
|
|
class="name"
|
|
role="link"
|
|
tabindex="0"
|
|
:aria-label="workspace.nameDisplay"
|
|
:to="workspace.detailLocation || {}"
|
|
>
|
|
{{ workspace.nameDisplay }}
|
|
</router-link>
|
|
</h2>
|
|
<div class="body">
|
|
<ResourcePanel
|
|
v-if="workspace.repos?.length || workspace.helmOps?.length"
|
|
:data-testid="'resource-panel-applications'"
|
|
:states="applicationStates[workspace.id]"
|
|
:workspace="workspace.id"
|
|
:type="FLEET.APPLICATION"
|
|
:selected-states="stateFilter[workspace.id] || {}"
|
|
@click:state="selectStates(workspace.id, $event)"
|
|
/>
|
|
<ResourcePanel
|
|
v-if="workspace.clusters?.length"
|
|
:data-testid="'resource-panel-clusters'"
|
|
:states="clusterStates[workspace.id]"
|
|
:workspace="workspace.id"
|
|
:type="FLEET.CLUSTER"
|
|
:selectable="false"
|
|
/>
|
|
<ResourcePanel
|
|
v-if="workspace.clusterGroups?.length"
|
|
:data-testid="'resource-panel-cluster-groups'"
|
|
:states="clusterGroupsStates[workspace.id]"
|
|
:workspace="workspace.id"
|
|
:type="FLEET.CLUSTER_GROUP"
|
|
:show-chart="false"
|
|
:selectable="false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="card-panel-main-actions">
|
|
<div
|
|
v-if="workspace.repos?.length || workspace.helmOps?.length"
|
|
class="expand-button"
|
|
:data-testid="'expand-button'"
|
|
>
|
|
<RcButton
|
|
small
|
|
ghost
|
|
:aria-label="`workspace-expand-btn-${ workspace.id }`"
|
|
@click="toggleCard(workspace.id)"
|
|
>
|
|
<i
|
|
:class="{
|
|
['icon icon-lg icon-chevron-down']: isWorkspaceCollapsed[workspace.id],
|
|
['icon icon-lg icon-chevron-up']: !isWorkspaceCollapsed[workspace.id],
|
|
}"
|
|
aria-hidden="true"
|
|
/>
|
|
</RcButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!isWorkspaceCollapsed[workspace.id] && (workspace.repos?.length || workspace.helmOps?.length)"
|
|
class="panel-expand mt-10"
|
|
:data-testid="`fleet-dashboard-expanded-panel-${ workspace.id }`"
|
|
>
|
|
<div class="actions">
|
|
<div class="type-filters">
|
|
<Checkbox
|
|
:data-testid="'fleet-dashboard-filter-git-repos'"
|
|
:value="typeFilter[workspace.id]?.[FLEET.GIT_REPO]"
|
|
@update:value="selectType(workspace.id, FLEET.GIT_REPO, $event)"
|
|
>
|
|
<template #label>
|
|
<i class="icon icon-lg icon-git mr-5" />
|
|
<span class="label">{{ t('fleet.dashboard.cards.filters.gitRepos') }}</span>
|
|
</template>
|
|
</Checkbox>
|
|
<Checkbox
|
|
:data-testid="'fleet-dashboard-filter-helm-ops'"
|
|
:value="typeFilter[workspace.id]?.[FLEET.HELM_OP]"
|
|
@update:value="selectType(workspace.id, FLEET.HELM_OP, $event)"
|
|
>
|
|
<template #label>
|
|
<i class="icon icon-lg icon-helm mr-5" />
|
|
<span class="label">{{ t('fleet.dashboard.cards.filters.helmOps') }}</span>
|
|
</template>
|
|
</Checkbox>
|
|
</div>
|
|
<div
|
|
v-if="permissions.gitRepos || permissions.helmOps"
|
|
class="create-button"
|
|
>
|
|
<RcButton
|
|
small
|
|
@click="createResource(workspace.id)"
|
|
>
|
|
{{ t('fleet.application.intro.add') }}
|
|
</RcButton>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="viewMode === 'cards'"
|
|
class="cards-panel"
|
|
>
|
|
<div
|
|
v-for="(state, j) in applicationStates[workspace.id]"
|
|
:key="j"
|
|
:data-testid="`state-panel-${ state.stateDisplay }`"
|
|
>
|
|
<div
|
|
v-if="cardResources[workspace.id][state.stateDisplay]?.length"
|
|
class="card-panel"
|
|
>
|
|
<div
|
|
role="button"
|
|
tabindex="0"
|
|
class="title"
|
|
:aria-label="`state-expand-btn-${ state.stateDisplay }`"
|
|
@click="toggleState(workspace.id, state.stateDisplay)"
|
|
@keydown.space.enter.stop.prevent="toggleState(workspace.id, state.stateDisplay)"
|
|
>
|
|
<i
|
|
:class="{
|
|
['icon icon-chevron-right']: isStateCollapsed[workspace.id]?.[state.stateDisplay],
|
|
['icon icon-chevron-down']: !isStateCollapsed[workspace.id]?.[state.stateDisplay],
|
|
}"
|
|
/>
|
|
<i
|
|
v-if="state.statePanel.id !== 'success'"
|
|
class="state-icon"
|
|
:class="state.statePanel.icon"
|
|
:style="{ color: state.statePanel.color }"
|
|
/>
|
|
<h3 class="state-title">
|
|
<span class="state-label">
|
|
{{ state.stateDisplay }}
|
|
</span>
|
|
<span class="state-amount">
|
|
{{ cardResources[workspace.id]?.[state.stateDisplay]?.length }}
|
|
</span>
|
|
<span class="total label-secondary">/{{ [ ...workspace.repos, ...workspace.helmOps ].length }}</span>
|
|
</h3>
|
|
</div>
|
|
<div
|
|
v-if="!isStateCollapsed[workspace.id]?.[state.stateDisplay]"
|
|
class="card-panel-body"
|
|
>
|
|
<div class="resource-cards-container">
|
|
<div
|
|
v-for="(item, y) in cardResources[workspace.id][state.stateDisplay]"
|
|
:key="y"
|
|
class="resource-card"
|
|
:class="{
|
|
['selected']: selectedCard === `${ item.id }-${ y }` && isOpenSlideInPanel
|
|
}"
|
|
:data-testid="`card-${ item.id }`"
|
|
>
|
|
<ResourceCard
|
|
v-if="y < (cardsCount[workspace.id]?.[state.stateDisplay] || CARDS_MIN)"
|
|
role="button"
|
|
tabindex="0"
|
|
:aria-label="`resource-card-${ item.id }`"
|
|
:data-testid="`resource-card-${ item.id }`"
|
|
:value="item"
|
|
:state-panel="state.statePanel"
|
|
@click="showResourceDetails(item, state.statePanel, workspace, `${ item.id }-${ y }`)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="resource-cards-action">
|
|
<p
|
|
v-if="(cardsCount[workspace.id]?.[state.stateDisplay] || 0) > CARDS_MIN"
|
|
@click="loadLess(workspace.id, state.stateDisplay)"
|
|
>
|
|
{{ t('generic.showLess') }}
|
|
</p>
|
|
<div />
|
|
<p
|
|
v-if="cardResources[workspace.id][state.stateDisplay]?.length > (cardsCount[workspace.id]?.[state.stateDisplay] || CARDS_MIN)"
|
|
@click="loadMore(workspace.id, state.stateDisplay)"
|
|
>
|
|
{{ t('generic.showMore') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="viewMode === VIEW_MODE.TABLE"
|
|
class="table-panel"
|
|
>
|
|
<FleetApplications
|
|
:workspace="workspace.id"
|
|
:rows="tableResources[workspace.id]"
|
|
:schema="{
|
|
id: FLEET.APPLICATION,
|
|
type: 'schema'
|
|
}"
|
|
:loading="$fetchState.pending"
|
|
:use-query-params-for-simple-filtering="true"
|
|
:show-intro="false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.dashboard-main-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: end;
|
|
gap: 16px;
|
|
|
|
.collapse-all-btn {
|
|
width: 105px;
|
|
}
|
|
}
|
|
|
|
.dashboard-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
h1 {
|
|
margin: 0;
|
|
}
|
|
|
|
> div {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
i {
|
|
color: var(--primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border);
|
|
border-radius: 16px;
|
|
background-color: var(--body-bg);
|
|
box-shadow: none;
|
|
min-width: 500px;
|
|
padding: 16px;
|
|
|
|
:focus-visible {
|
|
@include focus-outline;
|
|
}
|
|
|
|
.card-panel-main {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: auto;
|
|
margin-bottom: auto;
|
|
|
|
.card-panel-main-details {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.workspace-title {
|
|
min-width: 150px;
|
|
margin: 0 32px 0 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.workspace-label {
|
|
font-size: 16px;
|
|
font-weight: normal;
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 0 0 2px 0;
|
|
|
|
.icon {
|
|
margin-right: 5px;
|
|
}
|
|
}
|
|
|
|
.name {
|
|
font-size: 21px;
|
|
}
|
|
}
|
|
|
|
.body {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
|
|
.spacer {
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-panel-main-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: end;
|
|
|
|
.expand-button {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.panel-expand {
|
|
animation: slideInOut 0.5s ease-in-out;
|
|
|
|
:focus-visible {
|
|
@include focus-outline;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
.type-filters {
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin-top: 5px;
|
|
|
|
.checkbox-outer-container {
|
|
width: fit-content;
|
|
}
|
|
|
|
.label {
|
|
margin-top: 2px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.icon {
|
|
padding: 2px;
|
|
font-size: 25px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.cards-panel {
|
|
.card-panel {
|
|
margin-top: 24px;
|
|
|
|
.title {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
width: fit-content;
|
|
margin-bottom: 12px;
|
|
|
|
.icon {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.state-icon,
|
|
.state-title {
|
|
font-size: 21px;
|
|
}
|
|
|
|
.state-icon {
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.state-title {
|
|
display: flex;
|
|
align-items: baseline;
|
|
margin: 0;
|
|
|
|
.state-amount {
|
|
margin-left: 4px
|
|
}
|
|
|
|
.total {
|
|
margin-left: 4px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
p {
|
|
font-size: small;
|
|
|
|
.icon {
|
|
line-height: -1px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-panel-body {
|
|
.resource-cards-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 16px;
|
|
min-height: 100%;
|
|
|
|
.resource-card {
|
|
cursor: pointer;
|
|
|
|
&.selected {
|
|
.dashboard-resource-card {
|
|
border: 2px solid var(--primary);
|
|
margin: 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.resource-cards-action {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
|
|
p {
|
|
width: fit-content;
|
|
margin-left: 15px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.table-panel {
|
|
margin-top: 20px;
|
|
}
|
|
}
|
|
}
|
|
|
|
p {
|
|
color: var(--primary);
|
|
margin-right: 2px;
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.label-secondary{
|
|
color: var(--label-secondary);
|
|
}
|
|
|
|
@keyframes slideInOut {
|
|
0% {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(-10px);
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.5;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
100% {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
</style>
|