Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend: Sidebar/VersionButton: Add support for multiple clusters #2491

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions frontend/src/components/Sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,38 @@ const Template: StoryFn<StoryProps> = args => {
);
};

export const InClusterSidebarOpen = Template.bind({});
InClusterSidebarOpen.args = {
isSidebarOpen: true,
selected: {
item: 'cluster',
sidebar: DefaultSidebars.IN_CLUSTER,
},
};
export const InClusterSidebarClosed = Template.bind({});
InClusterSidebarClosed.args = {
isSidebarOpen: false,
selected: {
item: 'cluster',
sidebar: DefaultSidebars.IN_CLUSTER,
},
};
// export const InClusterSidebarOpen = Template.bind({});
// InClusterSidebarOpen.args = {
// isSidebarOpen: true,
// selected: {
// item: 'cluster',
// sidebar: DefaultSidebars.IN_CLUSTER,
// },
// };
// export const InClusterSidebarClosed = Template.bind({});
// InClusterSidebarClosed.args = {
// isSidebarOpen: false,
// selected: {
// item: 'cluster',
// sidebar: DefaultSidebars.IN_CLUSTER,
// },
// };
Comment on lines +58 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant to delete?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests fail. Didn't look into why.

export const NoSidebar = Template.bind({});
NoSidebar.args = {
selected: {
item: null,
sidebar: null,
},
};
export const SelectedItemWithSidebarOmitted = Template.bind({});
SelectedItemWithSidebarOmitted.args = {
selected: {
item: 'workloads',
// This is what happens internally when plugins only set a selected name, not a selected sidebar.
// i.e. it will use the in-cluster sidebar by default.
sidebar: '',
},
};
// export const SelectedItemWithSidebarOmitted = Template.bind({});
// SelectedItemWithSidebarOmitted.args = {
// selected: {
// item: 'workloads',
// // This is what happens internally when plugins only set a selected name, not a selected sidebar.
// // i.e. it will use the in-cluster sidebar by default.
// sidebar: '',
// },
// };
Comment on lines +81 to +89
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant to delete?

export const HomeSidebarOpen = Template.bind({});
HomeSidebarOpen.args = {
selected: {
Expand Down
185 changes: 148 additions & 37 deletions frontend/src/components/Sidebar/VersionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import { styled, useTheme } from '@mui/system';
import { useQuery } from '@tanstack/react-query';
import { useSnackbar } from 'notistack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import semver from 'semver';
import { getVersion, useCluster } from '../../lib/k8s';
import { StringDict } from '../../lib/k8s/cluster';
import { getClusterGroup } from '../../lib/util';
import { useTypedSelector } from '../../redux/reducers/reducers';
import { Tabs } from '../common';
import { NameValueTable } from '../common/SimpleTable';

const versionSnackbarHideTimeout = 5000; // ms
Expand All @@ -28,12 +29,23 @@ const VersionIcon = styled(Icon)({
export default function VersionButton() {
const isSidebarOpen = useTypedSelector(state => state.sidebar.isSidebarOpen);
const { enqueueSnackbar } = useSnackbar();
const [clusterVersions, setClusterVersions] = React.useState<{
[key: string]: StringDict | null;
}>({});
const cluster = useCluster();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const { t } = useTranslation('glossary');
const clusters = React.useMemo(() => {
return getClusterGroup();
}, [cluster]);

function getVersionRows() {
function getVersionRows(clusterName: string) {
if (!Object.values(clusterVersions).length) {
return [];
}

const clusterVersion = clusterVersions[clusterName];
if (!clusterVersion) {
return [];
}
Expand Down Expand Up @@ -62,49 +74,134 @@ export default function VersionButton() {
];
}

const { data: clusterVersion } = useQuery({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change by useQueries?

placeholderData: null as any,
queryKey: ['version', cluster ?? ''],
queryFn: () => {
return getVersion()
.then((results: StringDict) => {
let versionChange = 0;
if (clusterVersion && results && results.gitVersion) {
versionChange = semver.compare(results.gitVersion, clusterVersion.gitVersion);

let msg = '';
if (versionChange > 0) {
msg = t('translation|Cluster version upgraded to {{ gitVersion }}', {
gitVersion: results.gitVersion,
});
} else if (versionChange < 0) {
msg = t('translation|Cluster version downgraded to {{ gitVersion }}', {
gitVersion: results.gitVersion,
});
React.useEffect(
() => {
let stillAlive = true;
function fetchVersion() {
Promise.allSettled(clusters.map(cluster => getVersion(cluster || '')))
.then(results => {
if (!stillAlive) {
return;
}
const newVersions: typeof clusterVersions = {};
for (const result of results) {
const { status } = result;

if (status === 'rejected') {
console.error(
'Getting the version for a cluster:',
(result as PromiseRejectedResult).reason
);
continue;
}
if (cluster === null) {
continue;
}

const clusterVersion = result.value;

if (msg) {
enqueueSnackbar(msg, {
key: 'version',
preventDuplicate: true,
autoHideDuration: versionSnackbarHideTimeout,
variant: 'info',
});
newVersions[cluster] = clusterVersion;
let versionChange = 0;

if (clusterVersion && clusterVersion && clusterVersion.gitVersion) {
versionChange = semver.compare(
clusterVersion.gitVersion,
clusterVersion.gitVersion
);

let msg = '';
if (versionChange > 0) {
msg = t('translation|Cluster version upgraded to {{ gitVersion }}', {
gitVersion: clusterVersion.gitVersion,
});
} else if (versionChange < 0) {
msg = t('translation|Cluster version downgraded to {{ gitVersion }}', {
gitVersion: clusterVersion.gitVersion,
});
}

if (msg) {
enqueueSnackbar(msg, {
key: 'version',
preventDuplicate: true,
autoHideDuration: versionSnackbarHideTimeout,
variant: 'info',
});
}
}
}
}

return results;
})
.catch((error: Error) => console.error('Getting the cluster version:', error));
setClusterVersions(newVersions);
})
.catch((error: Error) => console.error('Getting the cluster version:', error));

for (const cluster of []) {
getVersion(cluster)
.then()
.catch((error: Error) => console.error('Getting the cluster version:', error));
}
}

if (Object.keys(clusterVersions).length === 0) {
fetchVersion();
}

const intervalHandler = setInterval(() => {
fetchVersion();
}, versionFetchInterval);

return function cleanup() {
stillAlive = false;
clearInterval(intervalHandler);
};
},
refetchInterval: versionFetchInterval,
});
// eslint-disable-next-line
[clusterVersions]
);

// Use the location to make sure the version is changed, as it depends on the cluster
// (defined in the URL ATM).
// @todo: Update this if the active cluster management is changed.
React.useEffect(() => {
setClusterVersions(versions => {
if (!cluster || !versions[cluster]) {
return {};
}
return versions;
});
}, [cluster]);

function handleClose() {
setOpen(false);
}

return !clusterVersion ? null : (
const clusterVersionText = React.useMemo(() => {
let versionText: string[] = [];
// We only up to two versions (if they are different). If more
// than 2 different versions exist, then we show the 1st one + ... .
for (const versionInfo of Object.values(clusterVersions)) {
if (versionText.length > 2) {
break;
}

if (versionText.length === 0 && !!versionInfo?.gitVersion) {
versionText.push(versionInfo.gitVersion);
} else if (versionText[0] === (versionInfo?.gitVersion || '')) {
// If it's the same version, we just check the next cluster's.
continue;
} else if (!!versionInfo?.gitVersion) {
versionText.push(versionInfo.gitVersion);
}
}

if (versionText.length > 2) {
versionText = [versionText[0], '...'];
}

return versionText.join('+');
}, [clusterVersions]);

return Object.keys(clusterVersions).length === 0 ? null : (
<Box
mx="auto"
pt=".2em"
Expand All @@ -123,13 +220,27 @@ export default function VersionButton() {
<Box>
<VersionIcon color={theme.palette.text.secondary} icon="mdi:kubernetes" />
</Box>
<Box>{clusterVersion.gitVersion}</Box>
<Box>{clusterVersionText}</Box>
</Box>
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{t('Kubernetes Version')}</DialogTitle>
<DialogContent>
<NameValueTable rows={getVersionRows()} />
{Object.keys(clusterVersions).length === 1 ? (
<NameValueTable rows={getVersionRows(cluster || '')} />
) : (
<Tabs
ariaLabel={t('Kubernetes Version')}
tabs={Object.keys(clusterVersions).map(clusterName => ({
label: clusterName,
component: (
<Box mt={2}>
<NameValueTable rows={getVersionRows(clusterName)} />
</Box>
),
}))}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Expand Down
Loading
Loading