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

feat: show navigation and zoom utilities in resources explorer #1070 #1145

Merged
123 changes: 110 additions & 13 deletions dashboard/components/explorer/DependencyGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, memo, useEffect } from 'react';
import React, { useState, memo, useEffect, useRef } from 'react';
import CytoscapeComponent from 'react-cytoscapejs';
import Cytoscape, { EdgeSingular, EventObject } from 'cytoscape';
import popper from 'cytoscape-popper';
Expand All @@ -16,6 +16,8 @@ import EmptyState from '@components/empty-state/EmptyState';

import Tooltip from '@components/tooltip/Tooltip';
import WarningIcon from '@components/icons/WarningIcon';
import DragIcon from '@components/icons/DragIcon';
import NumberInput from '@components/number-input/NumberInput';
import useInventory from '@components/inventory/hooks/useInventory/useInventory';
import settingsService from '@services/settingsService';
import InventorySidePanel from '@components/inventory/components/InventorySidePanel';
Expand All @@ -29,6 +31,7 @@ import {
minZoom,
nodeHTMLLabelConfig,
nodeStyeConfig,
// popperStyleConfig,
zoomLevelBreakpoint
} from './config';

Expand All @@ -42,6 +45,12 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
const [initDone, setInitDone] = useState(false);
const dataIsEmpty: boolean = data.nodes.length === 0;

const [zoomLevel, setZoomLevel] = useState(minZoom);
const [zoomVal, setZoomVal] = useState(0); // debounced zoom state to display percentage

const [isNodeDraggingEnabled, setNodeDraggingEnabled] = useState(true);

const cyRef = useRef<Cytoscape.Core | null>(null);
const {
openModal,
isOpen,
Expand Down Expand Up @@ -141,7 +150,10 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {

// Hide labels when being zoomed out
cy.on('zoom', event => {
if (cy.zoom() <= zoomLevelBreakpoint) {
const newZoomLevel = event.cy.zoom();
// setZoomLevel(newZoomLevel);

if (newZoomLevel <= zoomLevelBreakpoint) {
interface ExtendedEdgeSingular extends EdgeSingular {
popperRefObj?: any;
}
Expand All @@ -155,10 +167,13 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
});
}

// update state with new zoom level
setZoomLevel(newZoomLevel);

const opacity = cy.zoom() <= zoomLevelBreakpoint ? 0 : 1;

Array.from(
document.querySelectorAll('.dependency-graph-node-label'),
document.querySelectorAll('.dependency-graph-nodeLabel'),
e => {
// @ts-ignore
e.style.opacity = opacity;
Expand All @@ -171,6 +186,51 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
}
};

useEffect(() => {
const zoomPercentage = Math.round(
((zoomLevel - minZoom) / (maxZoom - minZoom)) * 100
);
const handler = setTimeout(() => {
setZoomVal(zoomPercentage);
}, 100); // 100ms debounce
return () => {
clearTimeout(handler);
};
}, [zoomLevel]);

const toggleNodeDragging = () => {
if (cyRef.current) {
if (isNodeDraggingEnabled) {
// to disable node dragging in Cytoscape
cyRef.current.nodes().ungrabify();
} else {
// to enable node dragging in Cytoscape
cyRef.current.nodes().grabify();
}
setNodeDraggingEnabled(!isNodeDraggingEnabled);
}
};

const handleZoomChange = (zoomPercentage: number) => {
let newZoomLevel = minZoom + zoomPercentage * ((maxZoom - minZoom) / 100);
if (newZoomLevel < minZoom) newZoomLevel = minZoom;
if (newZoomLevel > maxZoom) newZoomLevel = maxZoom;
if (cyRef.current) {
cyRef.current.zoom(newZoomLevel);
setZoomLevel(newZoomLevel);
}
};

let translateXClass;

if (zoomVal < 10) {
translateXClass = 'translate-x-1';
} else if (zoomVal >= 10 && zoomVal < 100) {
translateXClass = 'translate-x-2';
} else {
translateXClass = 'translate-x-3';
}

return (
<div className="relative h-full flex-1 bg-dependency-graph bg-[length:40px_40px]">
{dataIsEmpty ? (
Expand Down Expand Up @@ -208,21 +268,58 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
style: leafStyleConfig
}
]}
cy={(cy: Cytoscape.Core) => cyActionHandlers(cy)}
cy={(cy: Cytoscape.Core) => {
cyActionHandlers(cy);
cyRef.current = cy;
}}
/>
</>
)}
<div className="absolute bottom-0 left-0 flex gap-2 overflow-visible bg-black-100 text-black-400">
{data?.nodes?.length} Resources
{!dataIsEmpty && (
<div className="relative">
<WarningIcon className="peer" height="16" width="16" />
<Tooltip bottom="xs" align="left" width="lg">
Only AWS and Civo resources are currently supported on the
explorer.
<div className="absolute bottom-0 w-full">
<div className="flex w-full flex-col items-center gap-2 sm:flex-row sm:justify-between">
<div className="flex gap-2 overflow-visible bg-black-100 text-black-400">
{data?.nodes?.length} Resources
{!dataIsEmpty && (
<div className="relative">
<WarningIcon className="peer" height="16" width="16" />
<Tooltip bottom="xs" align="left" width="lg">
Only AWS resources are currently supported on the explorer.
</Tooltip>
</div>
)}
</div>
<div className="flex max-h-11 gap-4">
<button
className={`peer relative flex items-center rounded border-[1.2px] border-black-200 bg-white p-2.5 ${
isNodeDraggingEnabled && 'border-primary'
}`}
onClick={toggleNodeDragging}
>
<DragIcon className="h-6 w-6" />
</button>
<Tooltip align="center" bottom="sm">
{isNodeDraggingEnabled
? 'Disable node dragging'
: 'Enable node dragging'}
</Tooltip>

<div className="relative w-40">
<NumberInput
name="zoom"
value={zoomVal}
action={zoomData => handleZoomChange(Number(zoomData.zoom))}
handleValueChange={handleZoomChange} // increment or decrement input value
step={5} // percentage change in zoom
maxLength={3}
/>
<span
className={`absolute left-1/2 top-1/2 ${translateXClass} -translate-y-1/2 text-sm text-neutral-900`}
>
%
</span>
</div>
</div>
)}
</div>
</div>
{/* Modal */}
<InventorySidePanel
Expand Down
21 changes: 21 additions & 0 deletions dashboard/components/icons/DragIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SVGProps } from 'react';

const DragIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="22"
viewBox="0 0 20 22"
fill="none"
{...props}
>
<path
d="M9 3.63159V3.22106C9 2.88773 9.18056 1.57645 10.7363 1.57645C12.292 1.57645 12.3333 2.68771 12.5 3.02104V7.13159V4.13159C12.5 3.79826 12.6698 2.63159 14.1366 2.63159C15.7366 2.63159 16 3.63159 16 4.13159V7.63159V5.63159C16 5.29826 16.2924 4.13159 17.3814 4.13159C18.5814 4.13159 19 5.13159 19 5.63159C19 6.29826 19 8.03159 19 9.63159C19 11.6316 18.5 14.1316 18 16.1316C17.6 17.7316 16.1667 19.7983 15.5 20.6316H7.99999C4.49998 18.1316 2.5 13.1316 2 11.6316C1.5 10.1316 1.5 7.63159 2.5 6.63159C3.3 5.83159 4.83333 4.96493 5.49999 4.63159V11.1316V4.13159C5.5 3.13159 5.86913 2.15429 7.22482 2.15429C8.46938 2.15429 9 3.13159 9 3.63159ZM9 3.63159V7.13159V7.63159V3.63159Z"
stroke="#0C1717"
stroke-width="1"
stroke-linejoin="round"
/>
</svg>
);

export default DragIcon;
38 changes: 38 additions & 0 deletions dashboard/components/icons/Icons.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/react';
import * as icons from '@components/icons';
import { SVGProps } from 'react';
import Tooltip from '@components/tooltip/Tooltip';

// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction

const IconsWrapper = (props: SVGProps<SVGSVGElement>) => (
<div className="inline-flex w-full flex-wrap gap-2 p-2">
{Object.entries(icons).map(([name, Icon]) => (
<div key={name} className="relative">
<div className="peer flex h-full flex-col items-center justify-center gap-2 rounded-md border bg-white p-3">
<Icon {...props} />
<p className="text-sm">{name}</p>
</div>
<Tooltip align="center">{`import { ${name} } from "@components/icons"`}</Tooltip>
</div>
))}
</div>
);

const meta: Meta<typeof IconsWrapper> = {
title: 'Komiser/Icons',
component: IconsWrapper,
tags: ['autodocs'],
argTypes: {}
};

export default meta;
type Story = StoryObj<typeof IconsWrapper>;

// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Primary: Story = {
args: {
width: '24',
height: '24'
}
};
22 changes: 22 additions & 0 deletions dashboard/components/icons/MinusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
faisal7008 marked this conversation as resolved.
Show resolved Hide resolved

const MinusIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
{...props}
>
<path
d="M6 12.6318H18"
stroke="#0C1717"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

export default MinusIcon;
2 changes: 2 additions & 0 deletions dashboard/components/icons/PlusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const PlusIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="25"
fill="none"
{...props}
>
Expand Down
31 changes: 31 additions & 0 deletions dashboard/components/icons/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export { default as AlertIcon } from './AlertIcon';
export { default as ArrowDownIcon } from './ArrowDownIcon';
export { default as ArrowLeftIcon } from './ArrowLeftIcon';
export { default as BookmarkIcon } from './BookmarkIcon';
export { default as CheckIcon } from './CheckIcon';
export { default as ChevronDownIcon } from './ChevronDownIcon';
export { default as ChevronRightIcon } from './ChevronRightIcon';
export { default as ClearFilterIcon } from './ClearFilterIcon';
export { default as CloseIcon } from './CloseIcon';
export { default as DeleteIcon } from './DeleteIcon';
export { default as DocumentTextIcon } from './DocumentTextIcon';
export { default as DownloadIcon } from './DownloadIcon';
export { default as DragIcon } from './DragIcon';
export { default as DuplicateIcon } from './DuplicateIcon';
export { default as EditIcon } from './EditIcon';
export { default as ErrorIcon } from './ErrorIcon';
export { default as FilterIcon } from './FilterIcon';
export { default as Folder2Icon } from './Folder2Icon';
export { default as KeyIcon } from './KeyIcon';
export { default as LinkIcon } from './LinkIcon';
export { default as LoadingSpinner } from './LoadingSpinner';
export { default as MinusIcon } from './MinusIcon';
export { default as More2Icon } from './More2Icon';
export { default as PlusIcon } from './PlusIcon';
export { default as RecordCircleIcon } from './RecordCircleIcon';
export { default as RefreshIcon } from './RefreshIcon';
export { default as SearchIcon } from './SearchIcon';
export { default as ShieldSecurityIcon } from './ShieldSecurityIcon';
export { default as StarIcon } from './StarIcon';
export { default as VariableIcon } from './VariableIcon';
export { default as WarningIcon } from './WarningIcon';
Loading
Loading