Skip to content

Commit

Permalink
Merge pull request #1151 from faisal7008/feature/1072-nodedetails-sid…
Browse files Browse the repository at this point in the history
…epanel

feat: display node details in sidepanel #1072
  • Loading branch information
mlabouardy committed Oct 31, 2023
2 parents 049f9e0 + 2f0dce3 commit bc9bb99
Show file tree
Hide file tree
Showing 17 changed files with 597 additions and 85 deletions.
52 changes: 51 additions & 1 deletion dashboard/components/explorer/DependencyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import EmptyState from '@components/empty-state/EmptyState';

import Tooltip from '@components/tooltip/Tooltip';
import WarningIcon from '@components/icons/WarningIcon';
import useInventory from '@components/inventory/hooks/useInventory/useInventory';
import settingsService from '@services/settingsService';
import InventorySidePanel from '@components/inventory/components/InventorySidePanel';
import { ReactFlowData } from './hooks/useDependencyGraph';
import {
edgeAnimationConfig,
Expand All @@ -37,9 +40,36 @@ nodeHtmlLabel(Cytoscape.use(COSEBilkent));
Cytoscape.use(popper);
const DependencyGraph = ({ data }: DependencyGraphProps) => {
const [initDone, setInitDone] = useState(false);

const dataIsEmpty: boolean = data.nodes.length === 0;

const {
openModal,
isOpen,
closeModal,
data: inventoryItem,
page,
goTo,
tags,
handleChange,
addNewTag,
removeTag,
updateTags,
loading,
deleteLoading,
bulkItems,
updateBulkTags
} = useInventory();

// opens modal to display details of clicked node
const handleNodeClick = async (event: EventObject) => {
const nodeData = event.target.data();
settingsService.getResourceById(`?resourceId=${nodeData.id}`).then(res => {
if (res !== Error) {
openModal(res);
}
});
};

// Type technically is Cytoscape.EdgeCollection but that throws an unexpected error
const loopAnimation = (eles: any) => {
const ani = eles.animation(edgeAnimationConfig[0], edgeAnimationConfig[1]);
Expand Down Expand Up @@ -81,6 +111,8 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
cy.nodes().roots().addClass('root');
// Animate edges
cy.edges().forEach(loopAnimation);
// Add a click event listener to the Cytoscape graph
cy.on('tap', 'node', handleNodeClick);

// Add hover tooltip on edges
cy.edges().bind('mouseover', event => {
Expand Down Expand Up @@ -192,6 +224,24 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
</div>
)}
</div>
{/* Modal */}
<InventorySidePanel
isOpen={isOpen}
closeModal={closeModal}
data={inventoryItem}
goTo={goTo}
page={page}
updateTags={updateTags}
tags={tags}
tabs={['resource details', 'tags']}
handleChange={handleChange}
removeTag={removeTag}
addNewTag={addNewTag}
loading={loading}
deleteLoading={deleteLoading}
bulkItems={bulkItems}
updateBulkTags={updateBulkTags}
/>
</div>
);
};
Expand Down
22 changes: 22 additions & 0 deletions dashboard/components/icons/HyperLinkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';

const HyperLinkIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M13 11l8.2-8.2M22 6.8V2h-4.8M11 2H9C4 2 2 4 2 9v6c0 5 2 7 7 7h6c5 0 7-2 7-7v-2"
></path>
</svg>
);

export default HyperLinkIcon;
169 changes: 105 additions & 64 deletions dashboard/components/inventory/components/InventorySidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import SidepanelHeader from '@components/sidepanel/SidepanelHeader';
import SidepanelPage from '@components/sidepanel/SidepanelPage';
import Pill from '@components/pill/Pill';
import formatNumber from '../../../utils/formatNumber';
import providers from '../../../utils/providerHelper';
import Button from '../../button/Button';
Expand Down Expand Up @@ -27,6 +30,7 @@ type InventorySidePanelProps = {
isOpen: boolean;
bulkItems: [] | string[];
updateBulkTags: (action?: 'delete' | undefined) => void;
tabs: string[];
};

function InventorySidePanel({
Expand All @@ -43,80 +47,116 @@ function InventorySidePanel({
deleteLoading,
isOpen,
bulkItems,
updateBulkTags
updateBulkTags,
tabs
}: InventorySidePanelProps) {
const getLastFetched = (date: string) => {
const dateLastFetched = new Date(date);
const today = new Date();
const aMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
const aWeekAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
let message;
if (dateLastFetched > aMonthAgo) {
message = 'Since last month';
} else if (dateLastFetched > aWeekAgo) {
message = 'Since last week';
} else {
message = 'More than a month ago';
}
return message;
};

return (
<>
<Sidepanel isOpen={isOpen} closeModal={closeModal}>
{/* Modal headers */}
<div className="flex flex-wrap-reverse items-center justify-between gap-6 sm:flex-nowrap">
{data && (
<div className="flex flex-wrap items-center gap-4 sm:flex-nowrap">
<picture className="flex-shrink-0">
<img
src={providers.providerImg(data.provider)}
className="h-8 w-8 rounded-full"
alt={data.provider}
/>
</picture>

{data && (
<SidepanelHeader
title={data.service}
subtitle={data.name}
closeModal={closeModal}
href={data.link}
imgSrc={providers.providerImg(data.provider)}
imgAlt={data.provider}
>
{!data && bulkItems && (
<div className="flex flex-col gap-1 py-1">
<p className="... w-48 truncate font-medium text-black-900">
{data.service}
<p className="font-medium text-black-900">
Managing tags for {formatNumber(bulkItems.length)}{' '}
{bulkItems.length > 1 ? 'resources' : 'resource'}
</p>
<p className="flex items-center gap-2 text-xs text-black-300">
{data.name}
<a
target="_blank"
href={data.link}
rel="noreferrer"
className="hover:text-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M13 11l8.2-8.2M22 6.8V2h-4.8M11 2H9C4 2 2 4 2 9v6c0 5 2 7 7 7h6c5 0 7-2 7-7v-2"
></path>
</svg>
</a>
<p className="text-xs text-black-300">
All actions will overwrite previous tags for these resources
</p>
</div>
</div>
)}
{!data && bulkItems && (
<div className="flex flex-col gap-1 py-1">
<p className="font-medium text-black-900">
Managing tags for {formatNumber(bulkItems.length)}{' '}
{bulkItems.length > 1 ? 'resources' : 'resource'}
</p>
<p className="text-xs text-black-300">
All actions will overwrite previous tags for these resources
</p>
</div>
)}

<div className="flex flex-shrink-0 items-center gap-2">
<Button style="secondary" onClick={closeModal}>
Close
</Button>
</div>
</div>
)}
</SidepanelHeader>
)}

{/* Tabs */}
<SidepanelTabs goTo={goTo} page={page} tabs={['Tags']} />
<SidepanelTabs goTo={goTo} page={page} tabs={tabs} />

{/* Tab Content */}
{tabs.includes('resource details') && (
<SidepanelPage page={page} param={'resource details'}>
<div className="space-y-6 pt-1">
<div className="space-y-2">
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-500">
Cloud account
</h2>
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-900">
{!data && (
<p className="h-3 w-48 animate-pulse rounded-xl bg-komiser-200"></p>
)}
{data && <span>{data.account}</span>}
</h2>
</div>
<div className="space-y-2">
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-500">
Region
</h2>
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-900">
{!data && (
<p className="h-3 w-48 animate-pulse rounded-xl bg-komiser-200"></p>
)}
{data && <span>{data.region}</span>}
</h2>
</div>
<div className="space-y-2">
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-500">
Cost
</h2>
<h2 className=" flex items-center gap-2 text-sm">
{!data && (
<p className="h-3 w-48 animate-pulse rounded-xl bg-komiser-200"></p>
)}
{data && <span>{data?.cost.toFixed(2)}$</span>}
{data && (
<Pill status="removed">
{getLastFetched(data.fetchedAt)}
</Pill>
)}
</h2>
</div>
<div className="space-y-2">
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-500">
Relations
</h2>
<h2 className="font-['Noto Sans'] text-sm font-normal leading-tight text-neutral-900">
{!data && (
<p className="h-3 w-48 animate-pulse rounded-xl bg-komiser-200"></p>
)}
{data && (
<span>{data.relations.length} related resources</span>
)}
</h2>
</div>
</div>
</SidepanelPage>
)}
{/* Tags form */}
<div>
{page === 'tags' && (
{tabs.includes('tags') && (
<SidepanelPage page={page} param={'tags'}>
<form
onSubmit={e => {
e.preventDefault();
Expand All @@ -127,7 +167,7 @@ function InventorySidePanel({
updateTags();
}
}}
className="flex flex-col gap-6 pt-2"
className="flex flex-col gap-6 px-1 pt-2"
>
{tags &&
tags.map((tag, id) => (
Expand Down Expand Up @@ -185,8 +225,9 @@ function InventorySidePanel({
</Button>
</div>
</form>
)}

</SidepanelPage>
)}
<div>
{page === 'delete' && (
<>
<div className="flex flex-col gap-6 bg-black-100 p-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type Tag = {
};

export type InventoryItem = {
relations: any[];
account: string;
accountId: string;
cost: number;
Expand All @@ -55,7 +56,7 @@ export type InventoryItem = {
service: string;
tags: Tag[] | [] | null;
};
export type Pages = 'tags' | 'delete';
export type Pages = 'resource details' | 'tags' | 'delete';

export type View = {
id: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function useInventory() {
const [shouldFetchMore, setShouldFetchMore] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState<InventoryItem>();
const [page, setPage] = useState<Pages>('tags');
const [page, setPage] = useState<Pages>('resource details');
const [tags, setTags] = useState<Tag[]>();
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
Expand Down Expand Up @@ -461,7 +461,7 @@ function useInventory() {
*/
function cleanModal() {
setData(undefined);
setPage('tags');
setPage('resource details');
}

/** Opens the modal, as well as:
Expand Down
Loading

0 comments on commit bc9bb99

Please sign in to comment.