diff --git a/dashboard/components/explorer/DependencyGraph.tsx b/dashboard/components/explorer/DependencyGraph.tsx
index 6b115e14e..5fd719d14 100644
--- a/dashboard/components/explorer/DependencyGraph.tsx
+++ b/dashboard/components/explorer/DependencyGraph.tsx
@@ -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';
@@ -16,6 +16,11 @@ 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';
import { ReactFlowData } from './hooks/useDependencyGraph';
import {
edgeAnimationConfig,
@@ -26,6 +31,7 @@ import {
minZoom,
nodeHTMLLabelConfig,
nodeStyeConfig,
+ // popperStyleConfig,
zoomLevelBreakpoint
} from './config';
@@ -37,9 +43,42 @@ nodeHtmlLabel(Cytoscape.use(COSEBilkent));
Cytoscape.use(popper);
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(null);
+ 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]);
@@ -81,6 +120,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 => {
@@ -109,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;
}
@@ -123,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;
@@ -139,33 +186,53 @@ 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 (
- {/*
cyActionHandlers(cy)}
- /> */}
{dataIsEmpty ? (
<>
@@ -201,21 +268,77 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => {
style: leafStyleConfig
}
]}
- cy={(cy: Cytoscape.Core) => cyActionHandlers(cy)}
+ cy={(cy: Cytoscape.Core) => {
+ cyActionHandlers(cy);
+ cyRef.current = cy;
+ }}
/>
>
)}
-
- {data?.nodes?.length} Resources
- {!dataIsEmpty && (
-
-
-
- Only AWS resources are currently supported on the explorer.
+
+
+
+ {data?.nodes?.length} Resources
+ {!dataIsEmpty && (
+
+
+
+ Only AWS resources are currently supported on the explorer.
+
+
+ )}
+
+
+
+
+
+
+ {isNodeDraggingEnabled
+ ? 'Disable node dragging'
+ : 'Enable node dragging'}
+
+
+ handleZoomChange(Number(zoomData.zoom))}
+ handleValueChange={handleZoomChange} // increment or decrement input value
+ step={5} // percentage change in zoom
+ maxLength={3}
+ />
+
+ %
+
+
- )}
+
+ {/* Modal */}
+
);
};
diff --git a/dashboard/components/explorer/DependencyGraphWrapper.tsx b/dashboard/components/explorer/DependencyGraphWrapper.tsx
index 27034d0ed..8987dc347 100644
--- a/dashboard/components/explorer/DependencyGraphWrapper.tsx
+++ b/dashboard/components/explorer/DependencyGraphWrapper.tsx
@@ -110,7 +110,7 @@ function DependencyGraphWrapper() {
);
}}
action={() => {
- router.push('/');
+ router.push('/cloud-accounts');
}}
/>
diff --git a/dashboard/components/explorer/config.ts b/dashboard/components/explorer/config.ts
index df3dd13ec..5b4635235 100644
--- a/dashboard/components/explorer/config.ts
+++ b/dashboard/components/explorer/config.ts
@@ -64,10 +64,16 @@ export const nodeStyeConfig = {
'text-opacity': 1,
'font-size': 17,
'background-color': 'white',
- 'background-image': node =>
- node.data('provider') === 'AWS'
- ? '/assets/img/dependency-graph/aws-node.svg'
- : '',
+ 'background-image': node => {
+ switch (node.data('provider')) {
+ case 'AWS':
+ return '/assets/img/dependency-graph/aws-node.svg';
+ case 'Civo':
+ return '/assets/img/dependency-graph/civo-node.svg';
+ default:
+ return '';
+ }
+ },
'background-height': 20,
'background-width': 20,
'border-color': '#EDEBEE',
diff --git a/dashboard/components/icons/DragIcon.tsx b/dashboard/components/icons/DragIcon.tsx
new file mode 100644
index 000000000..6bd240499
--- /dev/null
+++ b/dashboard/components/icons/DragIcon.tsx
@@ -0,0 +1,21 @@
+import { SVGProps } from 'react';
+
+const DragIcon = (props: SVGProps
) => (
+
+
+
+);
+
+export default DragIcon;
diff --git a/dashboard/components/icons/HyperLinkIcon.tsx b/dashboard/components/icons/HyperLinkIcon.tsx
new file mode 100644
index 000000000..4f6500180
--- /dev/null
+++ b/dashboard/components/icons/HyperLinkIcon.tsx
@@ -0,0 +1,22 @@
+import { SVGProps } from 'react';
+
+const HyperLinkIcon = (props: SVGProps) => (
+
+
+
+);
+
+export default HyperLinkIcon;
diff --git a/dashboard/components/icons/Icons.stories.tsx b/dashboard/components/icons/Icons.stories.tsx
new file mode 100644
index 000000000..dc9fa689a
--- /dev/null
+++ b/dashboard/components/icons/Icons.stories.tsx
@@ -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) => (
+
+ {Object.entries(icons).map(([name, Icon]) => (
+
+
+
{`import { ${name} } from "@components/icons"`}
+
+ ))}
+
+);
+
+const meta: Meta = {
+ title: 'Komiser/Icons',
+ component: IconsWrapper,
+ tags: ['autodocs'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+// 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'
+ }
+};
diff --git a/dashboard/components/icons/MinusIcon.tsx b/dashboard/components/icons/MinusIcon.tsx
new file mode 100644
index 000000000..681b833f6
--- /dev/null
+++ b/dashboard/components/icons/MinusIcon.tsx
@@ -0,0 +1,22 @@
+import { SVGProps } from 'react';
+
+const MinusIcon = (props: SVGProps) => (
+
+
+
+);
+
+export default MinusIcon;
diff --git a/dashboard/components/icons/PlusIcon.tsx b/dashboard/components/icons/PlusIcon.tsx
index e0e5eed6f..47d072cf6 100644
--- a/dashboard/components/icons/PlusIcon.tsx
+++ b/dashboard/components/icons/PlusIcon.tsx
@@ -4,6 +4,8 @@ const PlusIcon = (props: SVGProps) => (
diff --git a/dashboard/components/icons/index.tsx b/dashboard/components/icons/index.tsx
new file mode 100644
index 000000000..c309a0205
--- /dev/null
+++ b/dashboard/components/icons/index.tsx
@@ -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';
diff --git a/dashboard/components/inventory/components/InventorySidePanel.tsx b/dashboard/components/inventory/components/InventorySidePanel.tsx
index 4b56a8723..9d20dcfa4 100644
--- a/dashboard/components/inventory/components/InventorySidePanel.tsx
+++ b/dashboard/components/inventory/components/InventorySidePanel.tsx
@@ -1,10 +1,12 @@
-import formatNumber from '../../../utils/formatNumber';
-import providers from '../../../utils/providerHelper';
-import Button from '../../button/Button';
-import CloseIcon from '../../icons/CloseIcon';
-import PlusIcon from '../../icons/PlusIcon';
-import Sidepanel from '../../sidepanel/Sidepanel';
-import SidepanelTabs from '../../sidepanel/SidepanelTabs';
+import SidepanelHeader from '@components/sidepanel/SidepanelHeader';
+import SidepanelPage from '@components/sidepanel/SidepanelPage';
+import Pill from '@components/pill/Pill';
+import Button from '@components/button/Button';
+import CloseIcon from '@components/icons/CloseIcon';
+import PlusIcon from '@components/icons/PlusIcon';
+import Sidepanel from '@components/sidepanel/Sidepanel';
+import SidepanelTabs from '@components/sidepanel/SidepanelTabs';
+import formatNumber from '@utils/formatNumber';
import {
InventoryItem,
Pages,
@@ -27,6 +29,7 @@ type InventorySidePanelProps = {
isOpen: boolean;
bulkItems: [] | string[];
updateBulkTags: (action?: 'delete' | undefined) => void;
+ tabs: string[];
};
function InventorySidePanel({
@@ -43,80 +46,123 @@ 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 (
<>
{/* Modal headers */}
-
- {data && (
-
-
-
-
-
+ {data && (
+
+ {!data && bulkItems && (
-
- {data.service}
+
+ Managing tags for {formatNumber(bulkItems.length)}{' '}
+ {bulkItems.length > 1 ? 'resources' : 'resource'}
-
- {data.name}
-
-
-
-
-
+
+ All actions will overwrite previous tags for these resources
-
- )}
- {!data && bulkItems && (
-
-
- Managing tags for {formatNumber(bulkItems.length)}{' '}
- {bulkItems.length > 1 ? 'resources' : 'resource'}
-
-
- All actions will overwrite previous tags for these resources
-
-
- )}
-
-
-
- Close
-
-
-
+ )}
+
+ )}
{/* Tabs */}
-
+
+ {/* Tab Content */}
+ {tabs.includes('resource details') && (
+
+
+
+
+ Cloud account
+
+
+ {!data && (
+
+ )}
+ {data &&
{data.account} }
+
+
+
+
+ Region
+
+
+ {!data && (
+
+ )}
+ {data &&
{data.region} }
+
+
+
+
+ Cost
+
+
+ {!data && (
+
+ )}
+ {data &&
{data?.cost.toFixed(2)}$ }
+ {data && (
+
+ {getLastFetched(data.fetchedAt)}
+
+ )}
+
+
+
+
+ Relations
+
+
+ {!data && (
+
+ )}
+ {data && (
+
{data.relations.length} related resources
+ )}
+
+
+
+
+ )}
{/* Tags form */}
-
- {page === 'tags' && (
+ {tabs.includes('tags') && (
+
- )}
-
+
+ )}
+
{page === 'delete' && (
<>
diff --git a/dashboard/components/inventory/components/InventoryTable.tsx b/dashboard/components/inventory/components/InventoryTable.tsx
index e07766e1e..8ab2cc48c 100644
--- a/dashboard/components/inventory/components/InventoryTable.tsx
+++ b/dashboard/components/inventory/components/InventoryTable.tsx
@@ -1,8 +1,8 @@
import { ToastProps } from '@components/toast/Toast';
import { NextRouter } from 'next/router';
import { ChangeEvent } from 'react';
+import Avatar from '@components/avatar/Avatar';
import formatNumber from '../../../utils/formatNumber';
-import providers from '../../../utils/providerHelper';
import Checkbox from '../../checkbox/Checkbox';
import SkeletonInventory from '../../skeleton/SkeletonInventory';
import {
@@ -118,13 +118,7 @@ function InventoryTable({
className="min-w-[7rem] cursor-pointer py-4 pl-2 pr-6"
>
-
-
-
+
{item.provider}
@@ -203,13 +197,7 @@ function InventoryTable({
className="min-w-[7rem] cursor-pointer py-4 pl-2 pr-6"
>
-
-
-
+
{item.provider}
diff --git a/dashboard/components/inventory/components/view/InventoryView.tsx b/dashboard/components/inventory/components/view/InventoryView.tsx
index 79b8b8288..7bf5b77a1 100644
--- a/dashboard/components/inventory/components/view/InventoryView.tsx
+++ b/dashboard/components/inventory/components/view/InventoryView.tsx
@@ -1,8 +1,9 @@
import Image from 'next/image';
import { NextRouter } from 'next/router';
import { ToastProps } from '@components/toast/Toast';
+import Avatar from '@components/avatar/Avatar';
import formatNumber from '../../../../utils/formatNumber';
-import providers, { Provider } from '../../../../utils/providerHelper';
+import { Provider } from '../../../../utils/providerHelper';
import Button from '../../../button/Button';
import Checkbox from '../../../checkbox/Checkbox';
import AlertIcon from '../../../icons/AlertIcon';
@@ -223,15 +224,7 @@ function InventoryView({
-
-
-
+
{item.provider}
diff --git a/dashboard/components/inventory/hooks/useInventory/types/useInventoryTypes.ts b/dashboard/components/inventory/hooks/useInventory/types/useInventoryTypes.ts
index cf4eb2b09..544133e08 100644
--- a/dashboard/components/inventory/hooks/useInventory/types/useInventoryTypes.ts
+++ b/dashboard/components/inventory/hooks/useInventory/types/useInventoryTypes.ts
@@ -40,6 +40,7 @@ export type Tag = {
};
export type InventoryItem = {
+ relations: any[];
account: string;
accountId: string;
cost: number;
@@ -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;
diff --git a/dashboard/components/inventory/hooks/useInventory/useInventory.tsx b/dashboard/components/inventory/hooks/useInventory/useInventory.tsx
index aace2643b..ceb6fb4ba 100644
--- a/dashboard/components/inventory/hooks/useInventory/useInventory.tsx
+++ b/dashboard/components/inventory/hooks/useInventory/useInventory.tsx
@@ -34,7 +34,7 @@ function useInventory() {
const [shouldFetchMore, setShouldFetchMore] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState
();
- const [page, setPage] = useState('tags');
+ const [page, setPage] = useState('resource details');
const [tags, setTags] = useState();
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
@@ -461,7 +461,7 @@ function useInventory() {
*/
function cleanModal() {
setData(undefined);
- setPage('tags');
+ setPage('resource details');
}
/** Opens the modal, as well as:
diff --git a/dashboard/components/number-input/NumberInput.stories.tsx b/dashboard/components/number-input/NumberInput.stories.tsx
new file mode 100644
index 000000000..db071c464
--- /dev/null
+++ b/dashboard/components/number-input/NumberInput.stories.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import NumberInput, { InputProps } from './NumberInput';
+
+// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
+
+const InputWrapper = ({
+ name,
+ label,
+ value,
+ action,
+ handleValueChange,
+ ...otherProps
+}: InputProps) => {
+ const [currValue, setCurrValue] = useState(0);
+ const handleChange = (newValue: number) => {
+ setCurrValue(newValue);
+ };
+ return (
+
+ handleChange(Number(newData.title))}
+ handleValueChange={handleChange}
+ {...otherProps}
+ />
+
+ );
+};
+
+const meta: Meta = {
+ title: 'Komiser/NumberInput',
+ component: InputWrapper,
+ tags: ['autodocs'],
+ argTypes: {
+ name: {
+ control: 'text',
+ description: 'the name for your form (if exist)',
+ defaultValue: 'input title'
+ },
+ label: {
+ control: 'text',
+ description: 'the label for your input (if exist)',
+ defaultValue: ''
+ },
+ disabled: {
+ control: 'boolean',
+ description: 'disables the input',
+ defaultValue: false
+ },
+ required: {
+ control: 'boolean',
+ description: 'Conditionally set the input field as required',
+ defaultValue: false
+ },
+ max: {
+ control: 'number',
+ description: 'the maximum value'
+ },
+ min: {
+ control: 'number',
+ description: 'the minimum value'
+ },
+ step: {
+ control: 'number',
+ description: 'change in value',
+ defaultValue: false
+ },
+ maxLength: {
+ control: 'number',
+ description: 'max length of the input'
+ }
+ }
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Small: Story = {
+ args: {
+ name: 'title',
+ label: ''
+ }
+};
+
+export const Large: Story = {
+ render: InputWrapper,
+ args: {
+ name: 'title',
+ label: 'Limit'
+ }
+};
diff --git a/dashboard/components/number-input/NumberInput.tsx b/dashboard/components/number-input/NumberInput.tsx
new file mode 100644
index 000000000..4100b7b8b
--- /dev/null
+++ b/dashboard/components/number-input/NumberInput.tsx
@@ -0,0 +1,135 @@
+import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from 'react';
+import MinusIcon from '@components/icons/MinusIcon';
+import PlusIcon from '@components/icons/PlusIcon';
+import { required } from '../../utils/regex';
+
+export type InputEvent = ChangeEvent;
+
+export type InputProps = {
+ disabled?: boolean;
+ id?: number;
+ name: string;
+ label?: string;
+ required?: boolean;
+ regex?: RegExp;
+ error?: string;
+ value: number;
+ autofocus?: boolean;
+ min?: number;
+ max?: number;
+ maxLength?: number;
+ positiveNumberOnly?: boolean;
+ action: (newData: any, id?: number) => void;
+ handleValueChange: (value: number) => void;
+ step?: number;
+};
+
+function NumberInput({
+ id,
+ name,
+ label,
+ regex = required,
+ error = 'Please provide a value',
+ autofocus,
+ positiveNumberOnly,
+ action,
+ handleValueChange,
+ value,
+ step = 1,
+ maxLength,
+ ...otherProps
+}: InputProps) {
+ const [isValid, setIsValid] = useState(undefined);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (autofocus) {
+ inputRef.current?.focus();
+ }
+ }, []);
+
+ function handleBlur(e: InputEvent): void {
+ const trimmedValue = e.target.value.trim();
+ if (!regex || !trimmedValue) return;
+
+ const testResult = regex.test(trimmedValue);
+ setIsValid(testResult);
+ }
+
+ function handleFocus(): void {
+ setIsValid(undefined);
+ }
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (positiveNumberOnly) {
+ const invalidChars = ['-', '+', 'e'];
+ if (invalidChars.includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+ }
+
+ const adjustBtn = `absolute ${
+ label ? 'w-14' : 'w-11'
+ } h-full p-3 border-gray-200 inline-flex justify-center items-center focus:outline-none`;
+
+ const iconStyle = `text-neutral-900 ${label ? 'w-8 h-8' : 'w-6 h-6'}`;
+
+ return (
+
+
+
handleValueChange(value - step)}
+ >
+
+
+
handleBlur(e)}
+ onChange={e => {
+ // e.target.value = e.target.value.slice(0, maxLength)
+ // if(Number(e.target.value) === 0) e.target.value = "0"
+ if (typeof id === 'number') {
+ action({ [name]: e.target.value }, id);
+ } else {
+ action({ [name]: e.target.value });
+ }
+ }}
+ onKeyDown={e => handleKeyDown(e)}
+ ref={inputRef}
+ autoComplete="off"
+ data-lpignore="true"
+ data-form-type="other"
+ value={value}
+ step={step}
+ {...otherProps}
+ />
+
handleValueChange(value + step)}
+ >
+
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {isValid === false && (
+
{error}
+ )}
+
+ );
+}
+
+export default NumberInput;
diff --git a/dashboard/components/onboarding-wizard/PurplinCloud.tsx b/dashboard/components/onboarding-wizard/PurplinCloud.tsx
index ea43c222c..b470fd871 100644
--- a/dashboard/components/onboarding-wizard/PurplinCloud.tsx
+++ b/dashboard/components/onboarding-wizard/PurplinCloud.tsx
@@ -1,7 +1,8 @@
import React from 'react';
import Image from 'next/image';
-import ProviderCls, { Provider } from '../../utils/providerHelper';
+import Avatar from '@components/avatar/Avatar';
+import { Provider } from '../../utils/providerHelper';
function PurplinCloud({ provider }: { provider: Provider }) {
return (
@@ -13,13 +14,7 @@ function PurplinCloud({ provider }: { provider: Provider }) {
height={120}
/>
);
diff --git a/dashboard/components/pill/Pill.mocks.tsx b/dashboard/components/pill/Pill.mocks.tsx
new file mode 100644
index 000000000..313f80ef3
--- /dev/null
+++ b/dashboard/components/pill/Pill.mocks.tsx
@@ -0,0 +1,48 @@
+import { PillProps } from './Pill';
+
+const active: PillProps = {
+ status: 'active',
+ children: 'active'
+};
+
+const pending: PillProps = {
+ status: 'pending',
+ children: 'pending'
+};
+
+const removed: PillProps = {
+ status: 'removed',
+ children: 'removed'
+};
+
+const inactive: PillProps = {
+ status: 'inactive',
+ children: 'inactive'
+};
+
+const info: PillProps = {
+ status: 'info',
+ children: 'info'
+};
+
+const latest: PillProps = {
+ status: 'new',
+ children: 'latest'
+};
+
+const highlight: PillProps = {
+ status: 'highlight',
+ children: 'highlight'
+};
+
+const mockPillProps = {
+ active,
+ pending,
+ removed,
+ inactive,
+ info,
+ latest,
+ highlight
+};
+
+export default mockPillProps;
diff --git a/dashboard/components/pill/Pill.stories.tsx b/dashboard/components/pill/Pill.stories.tsx
new file mode 100644
index 000000000..98c69ecd8
--- /dev/null
+++ b/dashboard/components/pill/Pill.stories.tsx
@@ -0,0 +1,57 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import Pill from './Pill';
+import mockPillProps from './Pill.mocks';
+
+// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
+const meta: Meta
= {
+ title: 'Komiser/Pill',
+ component: Pill,
+ tags: ['autodocs'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+export const Active: Story = {
+ args: {
+ ...mockPillProps.active
+ }
+};
+
+export const Pending: Story = {
+ args: {
+ ...mockPillProps.pending
+ }
+};
+
+export const Removed: Story = {
+ args: {
+ ...mockPillProps.removed
+ }
+};
+
+export const Inactive: Story = {
+ args: {
+ ...mockPillProps.inactive
+ }
+};
+
+export const Info: Story = {
+ args: {
+ ...mockPillProps.info
+ }
+};
+
+export const Latest: Story = {
+ args: {
+ ...mockPillProps.latest
+ }
+};
+
+export const Highlight: Story = {
+ args: {
+ ...mockPillProps.highlight
+ }
+};
diff --git a/dashboard/components/pill/Pill.tsx b/dashboard/components/pill/Pill.tsx
new file mode 100644
index 000000000..84309b45d
--- /dev/null
+++ b/dashboard/components/pill/Pill.tsx
@@ -0,0 +1,67 @@
+import { ReactNode } from 'react';
+
+export type PillProps = {
+ status:
+ | 'active'
+ | 'pending'
+ | 'removed'
+ | 'inactive'
+ | 'info'
+ | 'new'
+ | 'highlight';
+ children: ReactNode; // Remove the quotes
+ textcase?: 'uppercase' | 'lowercase';
+};
+
+function Pill({ status, children, textcase = 'lowercase' }: PillProps) {
+ const colors = {
+ active: {
+ background: 'bg-green-100',
+ text: 'text-green-400'
+ },
+ pending: {
+ background: 'bg-orange-100',
+ text: 'text-orange-400'
+ },
+ removed: {
+ background: 'bg-rose-100',
+ text: 'text-red-400'
+ },
+ inactive: {
+ background: 'bg-zinc-100',
+ text: 'text-zinc-400'
+ },
+ info: {
+ background: 'bg-blue-100',
+ text: 'text-blue-500'
+ },
+ new: {
+ background: 'bg-sky-100',
+ text: 'text-teal-600'
+ },
+ highlight: {
+ background: 'bg-violet-100',
+ text: 'text-violet-600'
+ }
+ };
+
+ const handleColor = () => colors[status];
+
+ return (
+
+ );
+}
+
+export default Pill;
diff --git a/dashboard/components/sidepanel/Sidepanel.stories.tsx b/dashboard/components/sidepanel/Sidepanel.stories.tsx
new file mode 100644
index 000000000..62b530a3f
--- /dev/null
+++ b/dashboard/components/sidepanel/Sidepanel.stories.tsx
@@ -0,0 +1,114 @@
+import { useEffect, useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import Button from '@components/button/Button';
+import Sidepanel from './Sidepanel';
+import SidepanelHeader from './SidepanelHeader';
+import SidepanelTabs from './SidepanelTabs';
+import SidepanelPage from './SidepanelPage';
+import SidepanelFooter from './SidepanelFooter';
+
+type SidepanelProps = {
+ title: string;
+ subtitle?: string;
+ href?: string;
+ imgSrc?: string;
+ imgAlt?: string;
+ deleteLabel?: string;
+ saveLabel?: string;
+ tabs: string[];
+};
+
+function SidepanelWrapper({
+ title,
+ tabs,
+ saveLabel,
+ ...others
+}: SidepanelProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [page, setPage] = useState('');
+
+ useEffect(() => {
+ if (tabs && tabs.length > 0) {
+ setPage(tabs[0]);
+ }
+ }, []);
+
+ const open = () => setIsOpen(true);
+ const close = () => setIsOpen(false);
+ const goTo = (newPage: string) => setPage(newPage);
+
+ return (
+
+
Open Sidepanel
+
+
+
+ {tabs &&
+ tabs.length > 0 &&
+ tabs?.map((tab: string) => (
+
+
+ Lorem ipsum, dolor sit amet consectetur adipisicing elit.
+ Tempora et officia tenetur est, minima veritatis doloremque
+ accusantium distinctio animi nulla reprehenderit quod asperiores
+ similique illum perferendis, reiciendis, ipsam doloribus sit!
+ Quidem amet veritatis ipsa omnis inventore architecto, assumenda
+ ad vero cupiditate pariatur natus nisi corporis. Nobis voluptas
+ vitae similique cupiditate deleniti. Inventore eos iusto porro
+ perspiciatis fugiat nam sequi eligendi voluptas autem. Vitae
+ beatae animi porro, fugiat eligendi hic cumque illum!
+ Consectetur culpa obcaecati dolore praesentium harum. Provident
+ nesciunt repudiandae eligendi quos, minima sed dolore veniam
+ consequatur delectus! Optio ratione cum dolor eaque
+ necessitatibus numquam maiores inventore asperiores quisquam
+ quidem?
+
+
+ ))}
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: 'Komiser/SidePanel',
+ component: SidepanelWrapper,
+ tags: ['autodocs'],
+ argTypes: {}
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ title: 'Resource name',
+ tabs: ['tab 1', 'tab 2', 'tab 3'],
+ subtitle: 'service',
+ href: 'https://docs.komiser.io/',
+ imgSrc: '/assets/img/providers/aws.png',
+ imgAlt: 'komiser',
+ deleteLabel: 'delete',
+ saveLabel: 'Save changes'
+ }
+};
+
+export const Secondary: Story = {
+ args: {
+ title: 'Create new policy',
+ tabs: ['tab 1', 'tab 2', 'tab 3'],
+ deleteLabel: 'delete',
+ saveLabel: 'Save changes'
+ }
+};
diff --git a/dashboard/components/sidepanel/Sidepanel.tsx b/dashboard/components/sidepanel/Sidepanel.tsx
index 9daf0cbdf..52bbbbe0d 100644
--- a/dashboard/components/sidepanel/Sidepanel.tsx
+++ b/dashboard/components/sidepanel/Sidepanel.tsx
@@ -32,7 +32,7 @@ function Sidepanel({ isOpen, closeModal, children, noScroll }: SidepanelProps) {
className="fixed inset-0 z-30 hidden animate-fade-in bg-black-900/10 opacity-0 sm:block"
>
diff --git a/dashboard/components/sidepanel/SidepanelFooter.tsx b/dashboard/components/sidepanel/SidepanelFooter.tsx
new file mode 100644
index 000000000..1da7fee56
--- /dev/null
+++ b/dashboard/components/sidepanel/SidepanelFooter.tsx
@@ -0,0 +1,37 @@
+import Button from '@components/button/Button';
+
+export type SidepanelFooterProps = {
+ loading?: boolean;
+ closeModal: () => void;
+ saveAction: () => void;
+ saveLabel?: string;
+};
+
+function SidepanelFooter({
+ closeModal,
+ saveAction,
+ saveLabel,
+ loading
+}: SidepanelFooterProps) {
+ return (
+ <>
+
+
+
+ Cancel
+
+
+ {saveLabel || 'Save'}
+
+
+
+ >
+ );
+}
+
+export default SidepanelFooter;
diff --git a/dashboard/components/sidepanel/SidepanelHeader.tsx b/dashboard/components/sidepanel/SidepanelHeader.tsx
index 6d2bfd6bc..089b406fe 100644
--- a/dashboard/components/sidepanel/SidepanelHeader.tsx
+++ b/dashboard/components/sidepanel/SidepanelHeader.tsx
@@ -1,32 +1,75 @@
-import Button from '../button/Button';
+import { ReactNode } from 'react';
+import ArrowLeftIcon from '@components/icons/ArrowLeftIcon';
+import HyperLinkIcon from '@components/icons/HyperLinkIcon';
+import Button from '@components/button/Button';
+import Avatar from '@components/avatar/Avatar';
+import { Provider } from '@utils/providerHelper';
-type SidepanelHeaderProps = {
+export type SidepanelHeaderProps = {
title: string;
- subtitle: string;
+ subtitle?: string;
+ href?: string;
+ cloudProvider?: Provider;
+ children?: ReactNode;
closeModal: () => void;
deleteAction?: () => void;
+ goBack?: () => void;
deleteLabel?: string;
};
function SidepanelHeader({
title,
subtitle,
+ href,
+ cloudProvider,
+ children,
closeModal,
deleteAction,
- deleteLabel
+ deleteLabel,
+ goBack
}: SidepanelHeaderProps) {
return (
-
-
-
-
- {title}
-
-
- {subtitle}
-
+
+ {title && subtitle && (
+
+ {cloudProvider &&
}
+
+
+ {title}
+
+
+
+
+
+ {subtitle}
+
+
-
+ )}
+
+ {title && !subtitle && (
+
+ )}
+
+ {children}
{deleteAction && (
diff --git a/dashboard/components/sidepanel/SidepanelPage.tsx b/dashboard/components/sidepanel/SidepanelPage.tsx
index c1571e72b..124fbe6e3 100644
--- a/dashboard/components/sidepanel/SidepanelPage.tsx
+++ b/dashboard/components/sidepanel/SidepanelPage.tsx
@@ -16,7 +16,11 @@ function SidepanelPage({
return (
<>
{page === param && (
-
+
)}
diff --git a/dashboard/components/sidepanel/SidepanelTabs.tsx b/dashboard/components/sidepanel/SidepanelTabs.tsx
index b1075a66f..5b40e4d9d 100644
--- a/dashboard/components/sidepanel/SidepanelTabs.tsx
+++ b/dashboard/components/sidepanel/SidepanelTabs.tsx
@@ -1,4 +1,6 @@
-type SidepanelTabsProps = {
+import { capitalizeString } from '@utils/formatString';
+
+export type SidepanelTabsProps = {
goTo: (page: any) => void;
page: string;
tabs: string[];
@@ -19,7 +21,7 @@ function SidepanelTabs({ goTo, page, tabs }: SidepanelTabsProps) {
: 'border-transparent hover:text-komiser-700'
}`}
>
- {tab}
+ {capitalizeString(tab)} {/* capitalize first letter */}
))}
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index d63afb53a..cf7e30e8b 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -59,13 +59,13 @@
"eslint-config-next": "13.5.4",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.4.2",
- "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-storybook": "^0.6.15",
"husky": "^8.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.6.4",
"postcss": "^8.4.31",
- "prettier": "^2.7.1",
+ "prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.4.5",
"tailwindcss": "^3.3.2"
@@ -3746,6 +3746,56 @@
"node": ">=14"
}
},
+ "node_modules/@pkgr/utils": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
+ "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.3.0",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@pkgr/utils/node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@pkgr/utils/node_modules/open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "dev": true,
+ "dependencies": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
@@ -7441,6 +7491,21 @@
"node": ">= 6"
}
},
+ "node_modules/@storybook/cli/node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/@storybook/client-api": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.4.3.tgz",
@@ -7611,6 +7676,21 @@
"url": "https://opencollective.com/storybook"
}
},
+ "node_modules/@storybook/codemod/node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/@storybook/components": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.4.3.tgz",
@@ -11611,6 +11691,21 @@
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
"dev": true
},
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dev": true,
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -13050,6 +13145,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dev": true,
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/default-browser-id": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
@@ -13066,6 +13179,116 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/default-browser/node_modules/execa": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/default-browser/node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/default-browser/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser/node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -14274,21 +14497,29 @@
}
},
"node_modules/eslint-plugin-prettier": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
- "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz",
+ "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==",
"dev": true,
"dependencies": {
- "prettier-linter-helpers": "^1.0.0"
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
},
"engines": {
- "node": ">=12.0.0"
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier"
},
"peerDependencies": {
- "eslint": ">=7.28.0",
- "prettier": ">=2.0.0"
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
"eslint-config-prettier": {
"optional": true
}
@@ -16652,6 +16883,39 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container/node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
@@ -20103,15 +20367,15 @@
}
},
"node_modules/prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
+ "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
- "prettier": "bin-prettier.js"
+ "prettier": "bin/prettier.cjs"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -21449,6 +21713,21 @@
"inherits": "^2.0.1"
}
},
+ "node_modules/run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -22535,6 +22814,22 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
@@ -23008,6 +23303,18 @@
"@popperjs/core": "^2.9.0"
}
},
+ "node_modules/titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
diff --git a/dashboard/package.json b/dashboard/package.json
index 06b1d6b48..39266758a 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -66,13 +66,13 @@
"eslint-config-next": "13.5.4",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.4.2",
- "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-storybook": "^0.6.15",
"husky": "^8.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.6.4",
"postcss": "^8.4.31",
- "prettier": "^2.7.1",
+ "prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.4.5",
"tailwindcss": "^3.3.2"
diff --git a/dashboard/pages/inventory.tsx b/dashboard/pages/inventory.tsx
index 5a9d6451b..4ef3017a4 100644
--- a/dashboard/pages/inventory.tsx
+++ b/dashboard/pages/inventory.tsx
@@ -173,6 +173,7 @@ export default function Inventory() {
deleteLoading={deleteLoading}
bulkItems={bulkItems}
updateBulkTags={updateBulkTags}
+ tabs={['resource details', 'tags']}
/>
{/* Error state */}
diff --git a/dashboard/pages/onboarding/choose-cloud.tsx b/dashboard/pages/onboarding/choose-cloud.tsx
index c57b9217e..4eed57ae0 100644
--- a/dashboard/pages/onboarding/choose-cloud.tsx
+++ b/dashboard/pages/onboarding/choose-cloud.tsx
@@ -3,10 +3,8 @@ import Head from 'next/head';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
-import ProviderCls, {
- allProviders,
- Provider
-} from '../../utils/providerHelper';
+import Avatar from '@components/avatar/Avatar';
+import platform, { allProviders, Provider } from '../../utils/providerHelper';
import Button from '../../components/button/Button';
import OnboardingWizardLayout, {
@@ -57,7 +55,7 @@ export default function ChooseCloud() {
values={Object.values(allProviders)}
handleChange={handleSelectChange}
displayValues={Object.values(allProviders).map(value => ({
- label: ProviderCls.providerLabel(value)
+ label: platform.getLabel(value)
}))}
/>
@@ -93,13 +91,7 @@ export default function ChooseCloud() {
height={120}
/>
(false);
@@ -75,16 +75,7 @@ export default function CloudAccounts() {
className="flex items-center justify-between rounded-lg border border-black-200 p-6"
>
-
-
-
-
+
@@ -92,7 +83,7 @@ export default function CloudAccounts() {
- {providers.providerLabel(account.provider)}
+ {platform.getLabel(account.provider)}
@@ -130,13 +121,7 @@ export default function CloudAccounts() {
@@ -148,13 +133,7 @@ export default function CloudAccounts() {
@@ -168,13 +147,7 @@ export default function CloudAccounts() {
@@ -188,13 +161,7 @@ export default function CloudAccounts() {
diff --git a/dashboard/public/assets/img/dependency-graph/civo-node.svg b/dashboard/public/assets/img/dependency-graph/civo-node.svg
new file mode 100644
index 000000000..3444c5daa
--- /dev/null
+++ b/dashboard/public/assets/img/dependency-graph/civo-node.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/public/assets/img/integrations/slack.png b/dashboard/public/assets/img/integrations/slack.png
new file mode 100644
index 000000000..3ab34c836
Binary files /dev/null and b/dashboard/public/assets/img/integrations/slack.png differ
diff --git a/dashboard/public/assets/img/integrations/webhook.png b/dashboard/public/assets/img/integrations/webhook.png
new file mode 100644
index 000000000..a8545e749
Binary files /dev/null and b/dashboard/public/assets/img/integrations/webhook.png differ
diff --git a/dashboard/public/assets/img/providers/aws.png b/dashboard/public/assets/img/providers/aws.png
index 899f5fa9e..a5569cec6 100644
Binary files a/dashboard/public/assets/img/providers/aws.png and b/dashboard/public/assets/img/providers/aws.png differ
diff --git a/dashboard/public/assets/img/providers/azure.png b/dashboard/public/assets/img/providers/azure.png
new file mode 100644
index 000000000..8c3266568
Binary files /dev/null and b/dashboard/public/assets/img/providers/azure.png differ
diff --git a/dashboard/public/assets/img/providers/azure.svg b/dashboard/public/assets/img/providers/azure.svg
deleted file mode 100644
index 7151406b9..000000000
--- a/dashboard/public/assets/img/providers/azure.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/dashboard/public/assets/img/providers/civo.jpeg b/dashboard/public/assets/img/providers/civo.jpeg
deleted file mode 100644
index 7fa0a8072..000000000
Binary files a/dashboard/public/assets/img/providers/civo.jpeg and /dev/null differ
diff --git a/dashboard/public/assets/img/providers/civo.png b/dashboard/public/assets/img/providers/civo.png
new file mode 100644
index 000000000..a79e3f43c
Binary files /dev/null and b/dashboard/public/assets/img/providers/civo.png differ
diff --git a/dashboard/public/assets/img/providers/digitalocean.png b/dashboard/public/assets/img/providers/digitalocean.png
index 7a7283069..a39c3383e 100644
Binary files a/dashboard/public/assets/img/providers/digitalocean.png and b/dashboard/public/assets/img/providers/digitalocean.png differ
diff --git a/dashboard/public/assets/img/providers/gcp.png b/dashboard/public/assets/img/providers/gcp.png
index 0a8bc5433..b6bb9198b 100644
Binary files a/dashboard/public/assets/img/providers/gcp.png and b/dashboard/public/assets/img/providers/gcp.png differ
diff --git a/dashboard/public/assets/img/providers/kubernetes.png b/dashboard/public/assets/img/providers/kubernetes.png
index e594696ac..96caf99da 100644
Binary files a/dashboard/public/assets/img/providers/kubernetes.png and b/dashboard/public/assets/img/providers/kubernetes.png differ
diff --git a/dashboard/public/assets/img/providers/linode.png b/dashboard/public/assets/img/providers/linode.png
index 8cbccf6c6..d8187cfa0 100644
Binary files a/dashboard/public/assets/img/providers/linode.png and b/dashboard/public/assets/img/providers/linode.png differ
diff --git a/dashboard/public/assets/img/providers/mongodbatlas.jpg b/dashboard/public/assets/img/providers/mongodbatlas.jpg
deleted file mode 100644
index ce779de32..000000000
Binary files a/dashboard/public/assets/img/providers/mongodbatlas.jpg and /dev/null differ
diff --git a/dashboard/public/assets/img/providers/mongodbatlas.png b/dashboard/public/assets/img/providers/mongodbatlas.png
new file mode 100644
index 000000000..7e3cfea0f
Binary files /dev/null and b/dashboard/public/assets/img/providers/mongodbatlas.png differ
diff --git a/dashboard/public/assets/img/providers/oci.png b/dashboard/public/assets/img/providers/oci.png
index 044939987..9570aa38e 100644
Binary files a/dashboard/public/assets/img/providers/oci.png and b/dashboard/public/assets/img/providers/oci.png differ
diff --git a/dashboard/public/assets/img/providers/pulumi.png b/dashboard/public/assets/img/providers/pulumi.png
new file mode 100644
index 000000000..d50127121
Binary files /dev/null and b/dashboard/public/assets/img/providers/pulumi.png differ
diff --git a/dashboard/public/assets/img/providers/scaleway.png b/dashboard/public/assets/img/providers/scaleway.png
index d595eb283..a2af3ada2 100644
Binary files a/dashboard/public/assets/img/providers/scaleway.png and b/dashboard/public/assets/img/providers/scaleway.png differ
diff --git a/dashboard/public/assets/img/providers/tencent.jpeg b/dashboard/public/assets/img/providers/tencent.jpeg
deleted file mode 100644
index 5ebdcb871..000000000
Binary files a/dashboard/public/assets/img/providers/tencent.jpeg and /dev/null differ
diff --git a/dashboard/public/assets/img/providers/tencent.png b/dashboard/public/assets/img/providers/tencent.png
new file mode 100644
index 000000000..69e5e6d1b
Binary files /dev/null and b/dashboard/public/assets/img/providers/tencent.png differ
diff --git a/dashboard/public/assets/img/providers/terraform.png b/dashboard/public/assets/img/providers/terraform.png
new file mode 100644
index 000000000..1566af689
Binary files /dev/null and b/dashboard/public/assets/img/providers/terraform.png differ
diff --git a/dashboard/services/settingsService.ts b/dashboard/services/settingsService.ts
index 5b4b5dd50..59d48e463 100644
--- a/dashboard/services/settingsService.ts
+++ b/dashboard/services/settingsService.ts
@@ -135,6 +135,19 @@ const settingsService = {
}
},
+ async getResourceById(urlParams: string) {
+ try {
+ const res = await fetch(
+ `${BASE_URL}/resources${urlParams}`,
+ settings('GET')
+ );
+ const data = await res.json();
+ return data;
+ } catch (error) {
+ return Error;
+ }
+ },
+
async getInventoryStats() {
try {
const res = await fetch(`${BASE_URL}/stats`, settings('GET'));
diff --git a/dashboard/styles/globals.css b/dashboard/styles/globals.css
index d4c2e6d9d..1e62db64b 100644
--- a/dashboard/styles/globals.css
+++ b/dashboard/styles/globals.css
@@ -23,6 +23,13 @@
.scrollbar::-webkit-scrollbar-thumb:hover {
background: #95a3a3;
}
+
+ /* Remove the default browser styles */
+ input[type='number']::-webkit-inner-spin-button,
+ input[type='number']::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
}
@variants responsive {
@@ -48,14 +55,8 @@
}
.popper-div {
- text-shadow:
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
- 0 0 5px #f4f9f9,
+ text-shadow: 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9,
+ 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9, 0 0 5px #f4f9f9,
0 0 5px #f4f9f9;
position: relative;
color: #000;
diff --git a/dashboard/utils/formatString.ts b/dashboard/utils/formatString.ts
new file mode 100644
index 000000000..436a3093a
--- /dev/null
+++ b/dashboard/utils/formatString.ts
@@ -0,0 +1,7 @@
+export function capitalizeString(inputString: string) {
+ if (inputString.length < 0) return inputString;
+
+ return (
+ inputString.charAt(0).toUpperCase() + inputString.slice(1).toLowerCase()
+ );
+}
diff --git a/dashboard/utils/providerHelper.ts b/dashboard/utils/providerHelper.ts
index 9e25de73e..8702b7929 100644
--- a/dashboard/utils/providerHelper.ts
+++ b/dashboard/utils/providerHelper.ts
@@ -9,7 +9,9 @@ export type Provider =
| 'tencent'
| 'oci'
| 'scaleway'
- | 'mongodbatlas';
+ | 'mongodbatlas'
+ | 'pulumi'
+ | 'terraform';
type ProviderKey =
| 'AWS'
@@ -22,7 +24,9 @@ type ProviderKey =
| 'TENCENT'
| 'OCI'
| 'SCALE_WAY'
- | 'MONGODB_ATLAS';
+ | 'MONGODB_ATLAS'
+ | 'PULUMI'
+ | 'TERRAFORM';
export const allProviders: { [key in ProviderKey]: Provider } = {
AWS: 'aws',
@@ -35,11 +39,12 @@ export const allProviders: { [key in ProviderKey]: Provider } = {
TENCENT: 'tencent',
OCI: 'oci',
SCALE_WAY: 'scaleway',
- MONGODB_ATLAS: 'mongodbatlas'
+ MONGODB_ATLAS: 'mongodbatlas',
+ TERRAFORM: 'terraform',
+ PULUMI: 'pulumi'
};
export type DBProvider = 'postgres' | 'sqlite';
-
type DBProviderKey = 'POSTGRES' | 'SQLITE';
export const allDBProviders: { [key in DBProviderKey]: DBProvider } = {
@@ -47,104 +52,103 @@ export const allDBProviders: { [key in DBProviderKey]: DBProvider } = {
SQLITE: 'sqlite'
};
-const providers = {
- providerLabel(arg: Provider) {
- let label;
-
- if (arg.toLowerCase() === 'aws') {
- label = 'Amazon Web Services';
- }
-
- if (arg.toLowerCase() === 'gcp') {
- label = 'Google Cloud Platform';
- }
- if (arg.toLowerCase() === 'digitalocean') {
- label = 'DigitalOcean';
- }
-
- if (arg.toLowerCase() === 'azure') {
- label = 'Azure';
- }
-
- if (arg.toLowerCase() === 'tencent') {
- label = 'Tencent';
- }
-
- if (arg.toLowerCase() === 'civo') {
- label = 'Civo';
- }
-
- if (arg.toLowerCase() === 'kubernetes') {
- label = 'Kubernetes';
- }
+export enum IntegrationProvider {
+ SLACK = 'slack',
+ WEBHOOK = 'webhook'
+}
- if (arg.toLowerCase() === 'linode') {
- label = 'Linode';
- }
-
- if (arg.toLowerCase() === 'oci') {
- label = 'OCI';
- }
+type ProviderInfo = {
+ label: string;
+ imgSrc: string;
+};
- if (arg.toLowerCase() === 'scaleway') {
- label = 'Scaleway';
- }
+export type Platform = {
+ cloudProviders: Record;
+ integrationProviders: Record;
+ getImgSrc: (providerName: Provider | IntegrationProvider) => string;
+ getLabel: (providerName: Provider | IntegrationProvider) => string;
+};
- if (arg.toLowerCase() === 'mongodbatlas') {
- label = 'MongoDB Atlas';
+const platform: Platform = {
+ cloudProviders: {
+ aws: {
+ label: 'Amazon Web Services',
+ imgSrc: '/assets/img/providers/aws.png'
+ },
+ gcp: {
+ label: 'Google Cloud Platform',
+ imgSrc: '/assets/img/providers/gcp.png'
+ },
+ digitalocean: {
+ label: 'DigitalOcean',
+ imgSrc: '/assets/img/providers/digitalocean.png'
+ },
+ azure: {
+ label: 'Azure',
+ imgSrc: '/assets/img/providers/azure.png'
+ },
+ civo: {
+ label: 'Civo',
+ imgSrc: '/assets/img/providers/civo.png'
+ },
+ kubernetes: {
+ label: 'Kubernetes',
+ imgSrc: '/assets/img/providers/kubernetes.png'
+ },
+ linode: {
+ label: 'Linode',
+ imgSrc: '/assets/img/providers/linode.png'
+ },
+ tencent: {
+ label: 'Tencent',
+ imgSrc: '/assets/img/providers/tencent.png'
+ },
+ oci: {
+ label: 'OCI',
+ imgSrc: '/assets/img/providers/oci.png'
+ },
+ scaleway: {
+ label: 'Scaleway',
+ imgSrc: '/assets/img/providers/scaleway.png'
+ },
+ mongodbatlas: {
+ label: 'MongoDB Atlas',
+ imgSrc: '/assets/img/providers/mongodbatlas.png'
+ },
+ terraform: {
+ label: 'Terraform',
+ imgSrc: '/assets/img/providers/terraform.png'
+ },
+ pulumi: {
+ label: 'Pulumi',
+ imgSrc: '/assets/img/providers/pulumi.png'
}
-
- return label;
},
- providerImg(arg: Provider) {
- let img;
-
- if (arg.toLowerCase() === 'aws') {
- img = '/assets/img/providers/aws.png';
- }
-
- if (arg.toLowerCase() === 'gcp') {
- img = '/assets/img/providers/gcp.png';
- }
-
- if (arg.toLowerCase() === 'digitalocean') {
- img = '/assets/img/providers/digitalocean.png';
- }
-
- if (arg.toLowerCase() === 'azure') {
- img = '/assets/img/providers/azure.svg';
- }
-
- if (arg.toLowerCase() === 'civo') {
- img = '/assets/img/providers/civo.jpeg';
- }
-
- if (arg.toLowerCase() === 'kubernetes') {
- img = '/assets/img/providers/kubernetes.png';
- }
-
- if (arg.toLowerCase() === 'linode') {
- img = '/assets/img/providers/linode.png';
- }
-
- if (arg.toLowerCase() === 'tencent') {
- img = '/assets/img/providers/tencent.jpeg';
- }
-
- if (arg.toLowerCase() === 'oci') {
- img = '/assets/img/providers/oci.png';
- }
-
- if (arg.toLowerCase() === 'scaleway') {
- img = '/assets/img/providers/scaleway.png';
- }
-
- if (arg.toLowerCase() === 'mongodbatlas') {
- img = '/assets/img/providers/mongodbatlas.jpg';
+ integrationProviders: {
+ slack: {
+ label: 'Slack',
+ imgSrc: '/assets/img/integrations/slack.png'
+ },
+ webhook: {
+ label: 'Custom Web-Hook',
+ imgSrc: '/assets/img/integrations/webhook.png'
}
+ },
- return img;
+ getImgSrc(providerName) {
+ const key = providerName.toLowerCase();
+ if (key in this.cloudProviders) return this.cloudProviders[key].imgSrc;
+ if (key in this.integrationProviders)
+ return this.integrationProviders[key].imgSrc;
+ return '';
+ },
+ getLabel(providerName) {
+ const key = providerName.toLowerCase();
+ if (key in this.cloudProviders) return this.cloudProviders[key].label;
+ if (key in this.integrationProviders)
+ return this.integrationProviders[key].label;
+ return '';
}
};
-export default providers;
+export default platform;
diff --git a/providers/civo/civo.go b/providers/civo/civo.go
index e658ecb79..715611f1a 100644
--- a/providers/civo/civo.go
+++ b/providers/civo/civo.go
@@ -49,7 +49,7 @@ func FetchResources(ctx context.Context, client providers.ProviderClient, db *bu
log.Printf("[%s][Civo] %s", client.Name, err)
} else {
for _, resource := range resources {
- _, err := db.NewInsert().Model(&resource).On("CONFLICT (resource_id) DO UPDATE").Set("cost = EXCLUDED.cost").Exec(context.Background())
+ _, err := db.NewInsert().Model(&resource).On("CONFLICT (resource_id) DO UPDATE").Set("cost = EXCLUDED.cost, relations=EXCLUDED.relations").Exec(context.Background())
if err != nil {
logrus.WithError(err).Errorf("db trigger failed")
}
diff --git a/providers/civo/network/loadbalancers.go b/providers/civo/network/loadbalancers.go
index d2db29475..9a998cf4f 100644
--- a/providers/civo/network/loadbalancers.go
+++ b/providers/civo/network/loadbalancers.go
@@ -29,7 +29,7 @@ func LoadBalancers(ctx context.Context, client providers.ProviderClient) ([]mode
ResourceId: lb.ID,
Cost: 10,
Name: lb.Name,
- Relations: relations,
+ Relations: relations,
FetchedAt: time.Now(),
Link: "https://dashboard.civo.com/loadbalancers",
})
@@ -46,18 +46,24 @@ func LoadBalancers(ctx context.Context, client providers.ProviderClient) ([]mode
}
func getLoadBalancerRelations(lb civogo.LoadBalancer) []models.Link {
- return []models.Link{
- {
+ var rel []models.Link
+
+ if len(lb.FirewallID) > 0 {
+ rel = append(rel, models.Link{
ResourceID: lb.FirewallID,
- Type: "Firewall",
- Name: lb.FirewallID, //cannot get the name of the network unless calling the network api
- Relation: "USES",
- },
- {
+ Type: "Firewall",
+ Name: lb.FirewallID, //cannot get the name of the network unless calling the network api
+ Relation: "USES",
+ })
+ }
+
+ if len(lb.FirewallID) > 0 {
+ rel = append(rel, models.Link{
ResourceID: lb.ClusterID,
- Type: "Cluster",
- Name: lb.ClusterID,
- Relation: "USES",
- },
+ Type: "Cluster",
+ Name: lb.ClusterID,
+ Relation: "USES",
+ })
}
-}
\ No newline at end of file
+ return rel
+}
diff --git a/providers/civo/storage/databases.go b/providers/civo/storage/databases.go
index f3d476526..3f103d1f3 100644
--- a/providers/civo/storage/databases.go
+++ b/providers/civo/storage/databases.go
@@ -41,7 +41,7 @@ func Databases(ctx context.Context, client providers.ProviderClient) ([]models.R
ResourceId: resource.ID,
Name: resource.Name,
Cost: monthlyCost,
- Relations: relations,
+ Relations: relations,
FetchedAt: time.Now(),
Link: fmt.Sprintf("https://dashboard.civo.com/databases/%s", resource.ID),
})
@@ -61,19 +61,23 @@ func getDatabaseRelation(db civogo.Database) []models.Link {
var rel []models.Link
- rel = append(rel, models.Link{
- ResourceID: db.NetworkID,
- Type: "Network",
- Name: db.NetworkID,
- Relation: "USES",
- })
+ if len(db.NetworkID) > 0 {
+ rel = append(rel, models.Link{
+ ResourceID: db.NetworkID,
+ Type: "Network",
+ Name: db.NetworkID,
+ Relation: "USES",
+ })
+ }
- rel = append(rel, models.Link{
- ResourceID: db.FirewallID,
- Type: "Firewall",
- Name: db.FirewallID,
- Relation: "USES",
- })
+ if len(db.FirewallID) > 0 {
+ rel = append(rel, models.Link{
+ ResourceID: db.FirewallID,
+ Type: "Firewall",
+ Name: db.FirewallID,
+ Relation: "USES",
+ })
+ }
- return rel
-}
\ No newline at end of file
+ return rel
+}
diff --git a/providers/civo/storage/volumes.go b/providers/civo/storage/volumes.go
index 2af5f4b8d..2924f6117 100644
--- a/providers/civo/storage/volumes.go
+++ b/providers/civo/storage/volumes.go
@@ -32,7 +32,7 @@ func Volumes(ctx context.Context, client providers.ProviderClient) ([]models.Res
ResourceId: volume.ID,
Cost: monthlyCost,
Name: volume.Name,
- Relations: relation,
+ Relations: relation,
FetchedAt: time.Now(),
CreatedAt: volume.CreatedAt,
Link: "https://dashboard.civo.com/volumes",
@@ -50,28 +50,34 @@ func Volumes(ctx context.Context, client providers.ProviderClient) ([]models.Res
}
func getVolumesRelation(vol civogo.Volume) []models.Link {
- var rel []models.Link
+ var rel []models.Link
- rel = append(rel, models.Link{
- ResourceID: vol.ClusterID,
- Type: "Kubernetes",
- Name: vol.ClusterID,
- Relation: "USES",
- })
+ if len(vol.ClusterID) > 0 {
+ rel = append(rel, models.Link{
+ ResourceID: vol.ClusterID,
+ Type: "Kubernetes",
+ Name: vol.ClusterID,
+ Relation: "USES",
+ })
+ }
+
+ if len(vol.InstanceID) > 0 {
+ rel = append(rel, models.Link{
+ ResourceID: vol.InstanceID,
+ Type: "Instance",
+ Name: vol.InstanceID,
+ Relation: "USES",
+ })
+ }
- rel = append(rel, models.Link{
- ResourceID: vol.InstanceID,
- Type: "Instance",
- Name: vol.InstanceID,
- Relation: "USES",
- })
-
- rel = append(rel, models.Link{
- ResourceID: vol.ClusterID,
- Type: "Network",
- Name: vol.NetworkID,
- Relation: "USES",
- })
+ if len(vol.ClusterID) > 0 {
+ rel = append(rel, models.Link{
+ ResourceID: vol.ClusterID,
+ Type: "Network",
+ Name: vol.NetworkID,
+ Relation: "USES",
+ })
+ }
return rel
-}
\ No newline at end of file
+}