Skip to content
Merged
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
215 changes: 214 additions & 1 deletion frontend/package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"type": "module",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@dagrejs/dagre": "^1.1.2",
Comment thread
illume marked this conversation as resolved.
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlamp-k8s/eslint-config": "^0.6.0",
Expand Down Expand Up @@ -45,12 +46,14 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.2.0",
"base64-arraybuffer": "^1.0.2",
"buffer": "^6.0.3",
"console-browserify": "^1.2.0",
"cronstrue": "^2.50.0",
"cross-env": "^7.0.3",
"crypto-browserify": "^3.12.0",
"elkjs": "^0.9.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.24.2",
Expand Down Expand Up @@ -94,7 +97,8 @@
"util": "^0.12.4",
"vite": "^5.4.9",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.2.0",
"web-worker": "^1.3.0"
},
"overrides": {
"domain-browser": "npm:dry-uninstall",
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/App/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default function Layout({}: LayoutProps) {
const arePluginsLoaded = useTypedSelector(state => state.plugins.loaded);
const dispatch = useDispatch();
const clusters = useTypedSelector(state => state.config.clusters);
const isFullWidth = useTypedSelector(state => state.ui.isFullWidth);
const { t } = useTranslation();
const allClusters = useClustersConf();
const clusterInURL = getCluster();
Expand Down Expand Up @@ -179,6 +180,10 @@ export default function Layout({}: LayoutProps) {
});
};

const containerProps = isFullWidth
? ({ maxWidth: false, disableGutters: true } as const)
: ({ maxWidth: 'xl' } as const);

return (
<>
<Link
Expand Down Expand Up @@ -213,7 +218,7 @@ export default function Layout({}: LayoutProps) {
<AlertNotification />
<Box>
<Div sx={theme.mixins.toolbar} />
<Container maxWidth="xl">
<Container {...containerProps}>
<NavigationTabs />
{arePluginsLoaded && (
<RouteSwitcher
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/App/RouteSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Route as RouteType,
} from '../../lib/router';
import { getCluster, getClusterGroup } from '../../lib/util';
import { setHideAppBar } from '../../redux/actions/actions';
import { setHideAppBar, setIsFullWidth } from '../../redux/actions/actions';
import { useTypedSelector } from '../../redux/reducers/reducers';
import ErrorBoundary from '../common/ErrorBoundary';
import ErrorComponent from '../common/ErrorPage';
Expand Down Expand Up @@ -83,6 +83,10 @@ function RouteComponent({ route }: { route: RouteType }) {
dispatch(setHideAppBar(route.hideAppBar));
}, [route.hideAppBar]);

React.useEffect(() => {
dispatch(setIsFullWidth(route.isFullWidth));
}, [route.isFullWidth]);

return (
<PageTitle
title={t(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,25 @@
</div>
</div>
</li>
<li
class="css-1lee0ix"
>
<a
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-padding MuiListItem-button css-sayhe9-MuiButtonBase-root-MuiListItem-root"
href="/"
role="button"
tabindex="0"
>
<div
aria-label="Map (beta)"
class="MuiListItemIcon-root css-1blhdvq-MuiListItemIcon-root"
data-mui-internal-clone-element="true"
/>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,32 @@
</div>
</div>
</li>
<li
class="css-1icvoo8"
>
<a
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-padding MuiListItem-button css-sayhe9-MuiButtonBase-root-MuiListItem-root"
href="/"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root css-1blhdvq-MuiListItemIcon-root"
/>
<div
class="MuiListItemText-root css-tlelie-MuiListItemText-root"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary css-nqgwvn-MuiTypography-root"
>
Map (beta)
</span>
</div>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,32 @@
</div>
</div>
</li>
<li
class="css-1icvoo8"
>
<a
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-padding MuiListItem-button css-sayhe9-MuiButtonBase-root-MuiListItem-root"
href="/"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root css-1blhdvq-MuiListItemIcon-root"
/>
<div
class="MuiListItemText-root css-tlelie-MuiListItemText-root"
>
<span
class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary css-nqgwvn-MuiTypography-root"
>
Map (beta)
</span>
</div>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</div>
<div
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/Sidebar/prepareRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ function prepareRoutes(
},
],
},
{
name: 'map',
icon: 'mdi:map',
label: t('glossary|Map (beta)'),
},
];

const sidebars: { [key: string]: SidebarItemProps[] } = {
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/components/resourceMap/GraphControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Icon } from '@iconify/react';
import { Box, Button, ButtonGroup } from '@mui/material';
import { useReactFlow, useStore } from '@xyflow/react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';

export function GraphControlButton({
children,
onClick,
title,
disabled,
}: {
children?: ReactNode;
title: string;
onClick: () => void;
disabled?: boolean;
}) {
const sx = {
width: '32px',
height: '32px',
padding: 0,
minWidth: '32px',
borderRadius: '50%',
'> svg': {
width: '14px',
height: '14px',
},
fontSize: 'x-small',
};

return (
<Button
disabled={disabled}
sx={sx}
color="primary"
variant="contained"
title={title}
onClick={onClick}
>
{children}
</Button>
);
}

export function GraphControls({ children }: { children?: React.ReactNode }) {
const { t } = useTranslation();
const minZoomReached = useStore(it => it.transform[2] <= it.minZoom);
const maxZoomReached = useStore(it => it.transform[2] >= it.maxZoom);
const { zoomIn, zoomOut, fitView } = useReactFlow();

return (
<Box display="flex" gap={1} flexDirection="column">
<ButtonGroup
sx={{
borderRadius: '40px',
'> .MuiButtonGroup-grouped': {
minWidth: '32px',
},
}}
orientation="vertical"
aria-label="Vertical button group"
variant="contained"
>
<GraphControlButton disabled={maxZoomReached} title={t('Zoom in')} onClick={() => zoomIn()}>
<Icon icon="mdi:plus" />
</GraphControlButton>
<GraphControlButton
disabled={minZoomReached}
title={t('Zoom out')}
onClick={() => zoomOut()}
>
<Icon icon="mdi:minus" />
</GraphControlButton>
</ButtonGroup>
<GraphControlButton title={t('Fit to screen')} onClick={() => fitView()}>
<Icon icon="mdi:fit-to-screen" />
</GraphControlButton>
{children}
</Box>
);
}
119 changes: 119 additions & 0 deletions frontend/src/components/resourceMap/GraphRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Box, Typography, useTheme } from '@mui/material';
import {
Background,
BackgroundVariant,
ConnectionMode,
Controls,
Edge,
EdgeMouseHandler,
Node,
NodeMouseHandler,
OnMoveStart,
ReactFlow,
} from '@xyflow/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Loader } from '../common';
import { KubeRelationEdge } from './edges/KubeRelationEdge';
import { GraphControls } from './GraphControls';
import { GroupNodeComponent } from './nodes/GroupNode';
import { KubeGroupNodeComponent } from './nodes/KubeGroupNode';
import { KubeObjectNodeComponent } from './nodes/KubeObjectNode';

export const nodeTypes = {
kubeObject: KubeObjectNodeComponent,
kubeGroup: KubeGroupNodeComponent,
group: GroupNodeComponent,
};

const edgeTypes = {
kubeRelation: KubeRelationEdge,
};

export interface GraphRendererProps {
/** List of nodes to render */
nodes: Node[];
/** List of edges to render */
edges: Edge[];
/** Callback when a node is clicked */
onNodeClick?: NodeMouseHandler<Node>;
/** Callback when an edge is clicked */
onEdgeClick?: EdgeMouseHandler<Edge>;
/** Callback when the graph is started to be moved */
onMoveStart?: OnMoveStart;
/** Callback when the background is clicked */
onBackgroundClick?: () => void;
/** Additional components to render */
children?: React.ReactNode;
/** Additional actions for the controls panael */
controlActions?: React.ReactNode;
isLoading?: boolean;
}

const emptyArray: any[] = [];

export function GraphRenderer({
nodes,
edges,
onNodeClick,
onEdgeClick,
onMoveStart,
onBackgroundClick,
children,
controlActions,
isLoading,
}: GraphRendererProps) {
const { t } = useTranslation();
const theme = useTheme();

return (
<ReactFlow
nodes={isLoading ? emptyArray : nodes}
edges={isLoading ? emptyArray : edges}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
nodesFocusable={false}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onMove={onMoveStart}
onClick={e => {
if ((e.target as HTMLElement)?.className?.includes?.('react-flow__pane')) {
onBackgroundClick?.();
}
}}
minZoom={0.1}
maxZoom={2.0}
connectionMode={ConnectionMode.Loose}
>
<Background variant={BackgroundVariant.Dots} style={{ color: theme.palette.divider }} />
<Controls showInteractive={false} showFitView={false} showZoom={false}>
<GraphControls>{controlActions}</GraphControls>
</Controls>
{isLoading && (
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<Loader title="Loading" />
</Box>
)}
{!isLoading && nodes.length === 0 && (
<Typography
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
{t('No data to be shown. Try to change filters or select a different namespace.')}
</Typography>
)}
{children}
</ReactFlow>
);
}
Loading