Add Dashboard page

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent 9b5bdeb85c
commit f8408469f7
27 changed files with 2680 additions and 3 deletions
@@ -0,0 +1,142 @@
import { ClusterNotFoundError } from '@shell/utils/error';
import { SETTING } from '@shell/config/settings';
import { COUNT, NAMESPACE, MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { DEV } from '@shell/store/prefs';
import { HCI } from '../../types';
export default {
async loadCluster({
state, commit, dispatch, getters, rootGetters, rootState
}: any, { id }: any) {
// This is a workaround for a timing issue where the mgmt cluster schema may not be available
// Try and wait until the schema exists before proceeding
await dispatch('management/waitForSchema', { type: MANAGEMENT.CLUSTER }, { root: true });
// See if it really exists
const cluster = await dispatch('management/find', {
type: MANAGEMENT.CLUSTER,
id,
opt: { url: `${ MANAGEMENT.CLUSTER }s/${ escape(id) }` }
}, { root: true });
let virtualBase = `/k8s/clusters/${ escape(id) }/v1/harvester`;
if (id === 'local') {
virtualBase = `/v1/harvester`;
}
if ( !cluster ) {
commit('clusterId', null, { root: true });
commit('applyConfig', { baseUrl: null });
throw new ClusterNotFoundError(id);
}
// Update the Steve client URLs
commit('applyConfig', { baseUrl: virtualBase });
await Promise.all([
dispatch('loadSchemas', true),
]);
dispatch('subscribe');
const projectArgs = {
type: MANAGEMENT.PROJECT,
opt: {
url: `${ MANAGEMENT.PROJECT }/${ escape(id) }`,
watchNamespace: id
}
};
const fetchProjects = async() => {
let limit = 30000;
const sleep = 100;
while ( limit > 0 && !rootState.managementReady ) {
await setTimeout(() => {}, sleep);
limit -= sleep;
}
if ( rootGetters['management/schemaFor'](MANAGEMENT.PROJECT) ) {
return dispatch('management/findAll', projectArgs, { root: true });
}
};
if (id !== 'local' && getters['schemaFor'](MANAGEMENT.SETTING)) { // multi-cluster
const settings = await dispatch('findAll', {
type: MANAGEMENT.SETTING,
id: SETTING.SYSTEM_NAMESPACES,
opt: { url: `${ virtualBase }/${ MANAGEMENT.SETTING }s/`, force: true }
});
const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES);
if (systemNamespaces) {
const namespace = (systemNamespaces.value || systemNamespaces.default)?.split(',');
commit('setSystemNamespaces', namespace, { root: true });
}
}
const hash: { [key: string]: Promise<any>} = {
projects: fetchProjects(),
virtualCount: dispatch('findAll', { type: COUNT }),
virtualNamespaces: dispatch('findAll', { type: NAMESPACE }),
settings: dispatch('findAll', { type: HCI.SETTING }),
clusters: dispatch('management/findAll', {
type: MANAGEMENT.CLUSTER,
opt: { force: true }
}, { root: true }),
};
if (getters['schemaFor'](HCI.UPGRADE)) {
hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE });
}
const res: any = await allHash(hash);
await dispatch('cleanNamespaces', null, { root: true });
commit('updateNamespaces', {
filters: [],
all: getters.filterNamespace(),
getters
}, { root: true });
// Solve compatibility with Rancher v2.6.x, fell remove these codes after not support v2.6.x
const definition = {
def: false,
parseJSON: true,
inheritFrom: DEV,
asUserPreference: true,
};
commit('prefs/setDefinition', {
name: 'view-in-api',
definition,
}, { root: true });
commit('prefs/setDefinition', {
name: 'all-namespaces',
definition,
}, { root: true });
commit('prefs/setDefinition', {
name: 'theme-shortcut',
definition,
}, { root: true });
commit('prefs/setDefinition', {
name: 'plugin-developer',
definition,
}, { root: true });
const isMultiCluster = !(res.clusters.length === 1 && res.clusters[0].metadata?.name === 'local');
if (isMultiCluster) {
commit('managementChanged', {
ready: true,
isMultiCluster: true,
isRancher: true,
}, { root: true });
}
},
};
@@ -0,0 +1,126 @@
import {
NAMESPACE_FILTER_KINDS,
NAMESPACE_FILTER_ALL as ALL,
NAMESPACE_FILTER_ALL_ORPHANS as ALL_ORPHANS,
} from '@shell/utils/namespace-filter';
import { MANAGEMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { filterBy } from '@shell/utils/array';
export default {
namespaceFilterOptions: (state: any, getters: any, rootState: any, rootGetters: any) => ({
addNamespace,
divider,
notFilterNamespaces
}: any) => {
const out = [{
id: ALL,
kind: NAMESPACE_FILTER_KINDS.SPECIAL,
label: rootGetters['i18n/t']('nav.ns.all'),
}];
divider(out);
const namespaces = getters.filterNamespace(notFilterNamespaces);
if (!rootGetters['isStandaloneHarvester'] && rootGetters['currentCluster'] && rootGetters['currentCluster']?.id !== '_') {
const cluster = rootGetters['currentCluster'];
let projects = rootGetters['management/all'](
MANAGEMENT.PROJECT
);
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
'nameDisplay',
]).filter((project: any) => project.nameDisplay !== 'System');
const projectsById: any = {};
const namespacesByProject: any = {};
let firstProject = true;
namespacesByProject['null'] = []; // For namespaces not in a project
for (const project of projects) {
projectsById[project.metadata.name] = project;
}
for (const namespace of namespaces) {
let projectId = namespace.projectId;
if (!projectId || !projectsById[projectId]) {
// If there's a projectId but that project doesn't exist, treat it like no project
projectId = 'null';
}
let entry = namespacesByProject[projectId];
if (!entry) {
entry = [];
namespacesByProject[namespace.projectId] = entry;
}
entry.push(namespace);
}
for (const project of projects) {
const id = project.metadata.name;
if (firstProject) {
firstProject = false;
} else {
divider(out);
}
out.push({
id: `project://${ id }`,
kind: 'project',
label: project.nameDisplay,
});
const forThisProject = namespacesByProject[id] || [];
addNamespace(out, forThisProject);
}
const orphans = namespacesByProject['null'];
if (orphans.length) {
if (!firstProject) {
divider(out);
}
out.push({
id: ALL_ORPHANS,
kind: 'project',
label: rootGetters['i18n/t']('nav.ns.orphan'),
});
addNamespace(out, orphans);
}
} else {
addNamespace(out, namespaces);
}
return out;
},
/**
* filter system/fleet/cattle namespace
*/
filterNamespace(state: any, getters: any, rootState: any, rootGetters: any, action: any) {
const allNamespaces = getters.all('namespace');
return (notFilterNamespaces: any = []) => {
return allNamespaces.filter((namespace: any) => {
return !namespace.isSystem || notFilterNamespaces.includes(namespace.id);
});
};
},
filterProject(state: any, getters: any, rootState: any, rootGetters: any) {
const projectsInAllClusters = rootGetters['management/all'](
MANAGEMENT.PROJECT
);
const currentCluster = rootGetters['currentCluster'];
const clusterId = currentCluster.id;
return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System');
}
};
@@ -0,0 +1,39 @@
import { CoreStoreSpecifics, CoreStoreConfig } from '@shell/core/types';
import { SteveFactory, steveStoreInit } from '@shell/plugins/steve/index';
import { PRODUCT_NAME } from '../../config/harvester';
import getters from './getters';
import mutations from './mutations';
import actions from './actions';
const harvesterFactory = (): CoreStoreSpecifics => {
const steveFactory = SteveFactory();
steveFactory.getters = {
...steveFactory.getters,
...getters,
};
steveFactory.mutations = {
...steveFactory.mutations,
...mutations,
};
steveFactory.actions = {
...steveFactory.actions,
...actions,
};
return steveFactory;
};
const config: CoreStoreConfig = {
namespace: PRODUCT_NAME,
isClusterStore: true
};
export default {
specifics: harvesterFactory(),
config,
init: steveStoreInit
};
@@ -0,0 +1 @@
export default {};
@@ -0,0 +1,36 @@
import { _MERGE } from '@shell/plugins/dashboard-store/actions';
import PollerSequential from '@shell/utils/poller-sequential';
const polling: any = {};
const POLL_INTERVAL = 10000;
export const actions = {
unsubscribe() {
Object.entries(polling).forEach(([type, poll]: [any, any]) => {
console.warn('Epinio: Polling stopped for: ', type); // eslint-disable-line no-console
poll.stop();
delete polling[type];
});
},
watch({ dispatch, rootGetters }: any, { type }: any) {
if (rootGetters['type-map/isSpoofed'](type) || polling[type]) {
// Ignore spoofed
return;
}
console.warn('Epinio: Polling started for: ', type);// eslint-disable-line no-console
polling[type] = new PollerSequential(
async() => {
console.debug('Epinio: Polling: ', type); // eslint-disable-line no-console
// NOTE - In order for lists to automatically update resources opt to MERGE data in place instead of replace
// (in rancher land these are all handled individually, here we have bulk changes)
await dispatch('findAll', { type, opt: { force: true, load: _MERGE } });
},
POLL_INTERVAL,
5
);
polling[type].start();
}
};