mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add Dashboard page
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user