diff --git a/.github/images/hacktoberfest-badge.svg b/.github/images/hacktoberfest-badge.svg new file mode 100644 index 000000000..747bbbb20 --- /dev/null +++ b/.github/images/hacktoberfest-badge.svg @@ -0,0 +1,3819 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/images/komiser-dashboard-new.png b/.github/images/komiser-dashboard-new.png new file mode 100644 index 000000000..699e5e3ce Binary files /dev/null and b/.github/images/komiser-dashboard-new.png differ diff --git a/.github/workflows/hacktoberfest-comment.yml b/.github/workflows/hacktoberfest-comment.yml new file mode 100644 index 000000000..1970c5143 --- /dev/null +++ b/.github/workflows/hacktoberfest-comment.yml @@ -0,0 +1,20 @@ +on: + pull_request: + types: [closed] + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Comment on PR + if: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'hacktoberfest-accepted') }} + uses: peter-evans/create-or-update-comment@v1 + with: + token: ${{ secrets.GH_ACTIONS }} + issue-number: ${{ github.event.pull_request.number }} + body: | + A heartfelt thank you, @${{ github.event.pull_request.user.login }}, for your enthusiastic involvement in Hacktoberfest! To express our gratitude, we've crafted a special virtual badge just for you. Don't hesitate to showcase it on your social media profiles and share the love by mentioning Komiser in your posts. Your contribution means the world to us! + ![Image](/images/hacktoberfest-badge.svg) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 41fb5f5c2..e28ef74b2 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,8 +1,8 @@ before: hooks: - #- go mod tidy - #- go-bindata-assetfs -o internal/api/v1/template.go out/... - #- sed -i -e 's/package main/package v1/g' internal/api/v1/template.go + - go mod tidy + - go-bindata-assetfs -o internal/api/v1/template.go out/... + - sed -i -e 's/package main/package v1/g' internal/api/v1/template.go builds: - env: - CGO_ENABLED=0 diff --git a/README.md b/README.md index 39dcc3ed1..f31056b19 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -**Komiser is back 🎉 and we're looking for maintainers to work on the new [roadmap](https://roadmap.tailwarden.com/), if you're interested, join us on our Discord community** - ---- -

Amp Logo

Komiser is an open-source cloud-agnostic resource manager designed to analyze and manage cloud cost, usage, security, and governance all in one place. It integrates seamlessly with multiple cloud providers, including AWS, Azure, Civo, Digital Ocean, OCI, Linode, Tencent, Scaleway and [more](#supported-cloud-providers). Interested? read more about Komiser on our [website](https://komiser.io?utm_source=github&utm_medium=social). @@ -68,7 +64,7 @@ Komiser is an open source project created to **analyse** and **manage cloud cost * Get a deep understanding of **how you spend** on the AWS, Azure, GCP, Civo, DigitalOcean and OCI. * Uncover idle and untagged resources, ensuring that no resource goes unnoticed. -

Komiser dashboard

+

Komiser dashboard

## Who is using it? Komiser was built with every Cloud Engineer, Developer, DevOps engineer and SRE in mind. We understand that tackling cost savings, security improvements and resource usage analyse efforts can be hard, sometimes just knowing where to start, can be the most challenging part at times. Komiser is here to help those cloud practitioners see their cloud resources and accounts much more clearly. Only with clear insight can timely and efficient actions take place. diff --git a/dashboard/components/avatar/Avatar.stories.tsx b/dashboard/components/avatar/Avatar.stories.tsx new file mode 100644 index 000000000..e2e128b5d --- /dev/null +++ b/dashboard/components/avatar/Avatar.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { allProviders, IntegrationProvider } from '@utils/providerHelper'; +import Avatar from './Avatar'; + +// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction +const meta: Meta = { + title: 'Komiser/Avatar', + component: Avatar, + tags: ['autodocs'], + args: { + size: 48 + } +}; + +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 AmazonWebServices: Story = { + args: { + avatarName: allProviders.AWS + } +}; + +export const GoogleCloudPlatform: Story = { + args: { + avatarName: allProviders.GCP + } +}; + +export const DigitalOcean: Story = { + args: { + avatarName: allProviders.DIGITAL_OCEAN + } +}; + +export const Azure: Story = { + args: { + avatarName: allProviders.AZURE + } +}; + +export const Civo: Story = { + args: { + avatarName: allProviders.CIVO + } +}; + +export const Kubernetes: Story = { + args: { + avatarName: allProviders.KUBERNETES + } +}; + +export const Linode: Story = { + args: { + avatarName: allProviders.LINODE + } +}; + +export const Tencent: Story = { + args: { + avatarName: allProviders.TENCENT + } +}; + +export const OCI: Story = { + args: { + avatarName: allProviders.OCI + } +}; + +export const Scaleway: Story = { + args: { + avatarName: allProviders.SCALE_WAY + } +}; + +export const MongoDBAtlas: Story = { + name: 'MongoDB Atlas', + args: { + avatarName: allProviders.MONGODB_ATLAS + } +}; + +export const Terraform: Story = { + args: { + avatarName: allProviders.TERRAFORM + } +}; + +export const Pulumi: Story = { + args: { + avatarName: allProviders.PULUMI + } +}; + +export const Slack: Story = { + args: { + avatarName: IntegrationProvider.SLACK + } +}; + +export const Webhook: Story = { + args: { + avatarName: IntegrationProvider.WEBHOOK + } +}; diff --git a/dashboard/components/avatar/Avatar.tsx b/dashboard/components/avatar/Avatar.tsx new file mode 100644 index 000000000..fd331daf4 --- /dev/null +++ b/dashboard/components/avatar/Avatar.tsx @@ -0,0 +1,22 @@ +import platform, { IntegrationProvider, Provider } from '@utils/providerHelper'; +import Image from 'next/image'; + +export type AvatarProps = { + avatarName: Provider | IntegrationProvider; + size?: number; +}; + +function Avatar({ avatarName, size = 24 }: AvatarProps) { + const src = platform.getImgSrc(avatarName) || 'unknown platform'; + return ( + {`${avatarName} + ); +} + +export default Avatar; diff --git a/dashboard/components/cloud-account/components/CloudAccountItem.tsx b/dashboard/components/cloud-account/components/CloudAccountItem.tsx index 82a0a3473..6ec687b35 100644 --- a/dashboard/components/cloud-account/components/CloudAccountItem.tsx +++ b/dashboard/components/cloud-account/components/CloudAccountItem.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; -import Image from 'next/image'; -import providers from '@utils/providerHelper'; +import Avatar from '@components/avatar/Avatar'; +import platform from '@utils/providerHelper'; import { CloudAccount } from '../hooks/useCloudAccounts/useCloudAccount'; import CloudAccountStatus from './CloudAccountStatus'; import More2Icon from '../../icons/More2Icon'; @@ -21,7 +21,7 @@ export default function CloudAccountItem({ setCloudAccountItem: (cloudAccountItem: CloudAccount) => void; }) { const optionsRef = useRef(null); - const { id, provider, name, status } = account; + const { id, provider: cloudProvider, name, status } = account; const [isOpen, setIsOpen] = useState(false); useEffect(() => { @@ -47,17 +47,10 @@ export default function CloudAccountItem({ onClick={() => openModal(account)} className="relative my-5 flex w-full items-center gap-4 rounded-lg border-2 border-black-170 bg-white p-6 text-black-900 transition-colors" > - {`${name} - +

{name}

-

{providers.providerLabel(provider)}

+

{platform.getLabel(cloudProvider)}

diff --git a/dashboard/components/cloud-account/components/CloudAccountsLayout.tsx b/dashboard/components/cloud-account/components/CloudAccountsLayout.tsx index ae617fb4d..d21791bb5 100644 --- a/dashboard/components/cloud-account/components/CloudAccountsLayout.tsx +++ b/dashboard/components/cloud-account/components/CloudAccountsLayout.tsx @@ -1,8 +1,8 @@ import { NextRouter } from 'next/router'; import { ReactNode, useContext } from 'react'; +import platform, { allProviders } from '@utils/providerHelper'; import GlobalAppContext from '../../layout/context/GlobalAppContext'; -import Providers, { allProviders } from '../../../utils/providerHelper'; import { CloudAccount } from '../hooks/useCloudAccounts/useCloudAccount'; type CloudAccountsLayoutProps = { @@ -73,7 +73,7 @@ function CloudAccountsLayout({ >

- {Providers.providerLabel(provider)} + {platform.getLabel(provider)}

diff --git a/dashboard/components/cloud-account/components/CloudAccountsSidePanel.tsx b/dashboard/components/cloud-account/components/CloudAccountsSidePanel.tsx index 8891462a3..05047608f 100644 --- a/dashboard/components/cloud-account/components/CloudAccountsSidePanel.tsx +++ b/dashboard/components/cloud-account/components/CloudAccountsSidePanel.tsx @@ -11,10 +11,8 @@ import OciAccountDetails from '@components/account-details/OciAccountDetails'; import ScalewayAccountDetails from '@components/account-details/ScalewayAccountDetails'; import { getPayloadFromForm } from '@utils/cloudAccountHelpers'; import { ToastProps } from '@components/toast/Toast'; -import providers, { - allProviders, - Provider -} from '../../../utils/providerHelper'; +import Avatar from '@components/avatar/Avatar'; +import { allProviders, Provider } from '../../../utils/providerHelper'; import AwsAccountDetails from '../../account-details/AwsAccountDetails'; import Button from '../../button/Button'; import Sidepanel from '../../sidepanel/Sidepanel'; @@ -132,14 +130,7 @@ function CloudAccountsSidePanel({
{cloudAccount && (
- - {cloudAccount.provider} - - +

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]) => ( +
+
+ +

{name}

+
+ {`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.provider} - - + {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 -

-
- )} - -
- -
-
+ )} + + )} {/* 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') && ( +
{ e.preventDefault(); @@ -127,7 +173,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) => ( @@ -185,8 +231,9 @@ function InventorySidePanel({
- )} - + + )} +
{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} - + {item.provider}
@@ -203,13 +197,7 @@ function InventoryTable({ className="min-w-[7rem] cursor-pointer py-4 pl-2 pr-6" >
- - {item.provider} - + {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} - + {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 ( +
+
+ + 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} + /> + + {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} />
- {`${provider} +
); 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 ( +
+

+ {children} +

+
+ ); +} + +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 ( +
+ + + + + {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 ( + <> +
+
+ + +
+
+ + ); +} + +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 && ( +
+ +
+

+ {title} +

+
+
+ )} + + {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 && ( -
+
{children}
)} 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} />
- {`${provider} +
(false); @@ -75,16 +75,7 @@ export default function CloudAccounts() { className="flex items-center justify-between rounded-lg border border-black-200 p-6" >
- - {account.provider} - - +

@@ -92,7 +83,7 @@ export default function CloudAccounts() {

- {providers.providerLabel(account.provider)} + {platform.getLabel(account.provider)}

@@ -130,13 +121,7 @@ export default function CloudAccounts() {
- AWS +
@@ -148,13 +133,7 @@ export default function CloudAccounts() {
- Civo +
@@ -168,13 +147,7 @@ export default function CloudAccounts() {
- GCP +
@@ -188,13 +161,7 @@ export default function CloudAccounts() {
- Azure +
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 +}