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: display node details in sidepanel #1072 #1151

Merged
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
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);
faisal7008 marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading