diff --git a/dashboard/components/button/Button.mocks.tsx b/dashboard/components/button/Button.mocks.tsx
index 736305a2a..a2251e8ac 100644
--- a/dashboard/components/button/Button.mocks.tsx
+++ b/dashboard/components/button/Button.mocks.tsx
@@ -8,7 +8,9 @@ const base: ButtonProps = {
size: 'lg',
disabled: false,
loading: false,
- onClick: () => {}
+ onClick: () => {},
+ href: '',
+ target: ''
};
const secondary: ButtonProps = {
@@ -63,13 +65,34 @@ const deleteButton: ButtonProps = {
onClick: () => {}
};
+const linkButton: ButtonProps = {
+ children: 'Link button',
+ asLink: true,
+ style: 'primary',
+ size: 'lg',
+ loading: false,
+ href: 'https://komiser.io'
+};
+
+const newTabLinkButton: ButtonProps = {
+ children: 'New Tab Link button',
+ asLink: true,
+ style: 'secondary',
+ size: 'lg',
+ loading: false,
+ href: 'https://komiser.io',
+ target: '_blank'
+};
+
const mockButtonProps = {
base,
secondary,
ghost,
text,
dropdown,
- deleteButton
+ deleteButton,
+ linkButton,
+ newTabLinkButton
};
export default mockButtonProps;
diff --git a/dashboard/components/button/Button.stories.tsx b/dashboard/components/button/Button.stories.tsx
index a83a23872..87e6eab4d 100644
--- a/dashboard/components/button/Button.stories.tsx
+++ b/dashboard/components/button/Button.stories.tsx
@@ -65,3 +65,15 @@ export const Delete: Story = {
...mockButtonProps.deleteButton
}
};
+
+export const Link: Story = {
+ args: {
+ ...mockButtonProps.linkButton
+ }
+};
+
+export const NewTabLink: Story = {
+ args: {
+ ...mockButtonProps.newTabLinkButton
+ }
+};
diff --git a/dashboard/components/button/Button.tsx b/dashboard/components/button/Button.tsx
index dedb7adec..6393f4673 100644
--- a/dashboard/components/button/Button.tsx
+++ b/dashboard/components/button/Button.tsx
@@ -12,10 +12,14 @@ export type ButtonProps = {
gap?: 'md';
transition?: boolean;
onClick?: (e?: any) => void;
+ asLink?: boolean;
+ href?: string;
+ target?: string;
};
function Button({
children,
+ asLink = false,
type = 'button',
style = 'primary',
size = 'md',
@@ -24,7 +28,9 @@ function Button({
align,
gap,
transition = true,
- onClick
+ onClick,
+ href,
+ target
}: ButtonProps) {
const xxs = 'p-1';
const xs = 'py-2 px-4';
@@ -73,21 +79,37 @@ function Button({
if (style === 'text') buttonStyle = text;
if (style === 'dropdown') buttonStyle = dropdown;
if (style === 'delete') buttonStyle = deleteStyle;
+ if (asLink) buttonStyle = `${buttonStyle} inline-block sm:w-fit-content`;
return buttonStyle;
}
return (
-
+ <>
+ {asLink ? (
+
+ {loading && }
+ {children}
+
+ ) : (
+
+ )}
+ >
);
}
diff --git a/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx b/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx
index c536d4801..cb6eb9a13 100644
--- a/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx
+++ b/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx
@@ -10,8 +10,11 @@ import {
import Image from 'next/image';
import { Dispatch, SetStateAction } from 'react';
import { Bar } from 'react-chartjs-2';
-import SelectCheckbox from '../../../select-checkbox/SelectCheckbox';
-import Select from '../../../select/Select';
+
+import Button from '@components/button/Button';
+import SelectCheckbox from '@components/select-checkbox/SelectCheckbox';
+import Select from '@components/select/Select';
+import { CloudIcon } from '@components/icons';
import {
CostExplorerQueryDateProps,
CostExplorerQueryGranularityProps,
@@ -115,17 +118,31 @@ function DashboardCostExplorerCard({
{chartData &&
}
{!chartData && (
-
-
- No data for this time period
-
-
+
+
+
+
No data for this time period
+
+ Our cloud version, Tailwarden, supports
+ historical costs from certain cloud providers
+
+
+
+
+
)}
diff --git a/dashboard/components/icons/CloudIcon.tsx b/dashboard/components/icons/CloudIcon.tsx
new file mode 100644
index 000000000..5ff0ca98f
--- /dev/null
+++ b/dashboard/components/icons/CloudIcon.tsx
@@ -0,0 +1,29 @@
+import { SVGProps } from 'react';
+
+const CloudIcon = (props: SVGProps
) => (
+
+);
+
+export default CloudIcon;
diff --git a/dashboard/components/icons/Icons.stories.tsx b/dashboard/components/icons/Icons.stories.tsx
index dc9fa689a..5c2da12fb 100644
--- a/dashboard/components/icons/Icons.stories.tsx
+++ b/dashboard/components/icons/Icons.stories.tsx
@@ -9,7 +9,7 @@ const IconsWrapper = (props: SVGProps) => (
{Object.entries(icons).map(([name, Icon]) => (
-
+
diff --git a/dashboard/components/icons/index.tsx b/dashboard/components/icons/index.tsx
index c309a0205..72f0b2681 100644
--- a/dashboard/components/icons/index.tsx
+++ b/dashboard/components/icons/index.tsx
@@ -7,6 +7,7 @@ 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 CloudIcon } from './CloudIcon';
export { default as DeleteIcon } from './DeleteIcon';
export { default as DocumentTextIcon } from './DocumentTextIcon';
export { default as DownloadIcon } from './DownloadIcon';
diff --git a/dashboard/pages/cloud-accounts.tsx b/dashboard/pages/cloud-accounts.tsx
index 1b7568579..79be7073b 100644
--- a/dashboard/pages/cloud-accounts.tsx
+++ b/dashboard/pages/cloud-accounts.tsx
@@ -14,16 +14,32 @@ import CloudAccountDeleteContents from '@components/cloud-account/components/Clo
import { useToast } from '@components/toast/ToastProvider';
import EmptyState from '@components/empty-state/EmptyState';
+import Banner from '@components/banner/Banner';
+import Button from '@components/button/Button';
function CloudAccounts() {
const [editCloudAccount, setEditCloudAccount] = useState
(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [isTailwardenBannerDismissed, setIsTailwardenBannerDismissed] =
+ useState(true);
+
const { toast, showToast, dismissToast } = useToast();
const router = useRouter();
const currentViewProvider = router.query.view as string;
+ const hideTailwardenBanner = () => {
+ setIsTailwardenBannerDismissed(true);
+ window.localStorage.setItem('tailwardenBannerDismissed', 'true');
+ };
+
+ useEffect(() => {
+ setIsTailwardenBannerDismissed(
+ window.localStorage.getItem('tailwardenBannerDismissed') === 'true'
+ );
+ }, []);
+
const {
cloudAccounts,
setCloudAccounts,
@@ -75,7 +91,7 @@ function CloudAccounts() {
{/* Wraps the cloud account page and handles the custom views sidebar */}
-
+
{filteredCloudAccounts.map(account => (
)}
+
+ {cloudAccounts.length >= 2 && !isTailwardenBannerDismissed && (
+
+ For deeper insights and account-level alerts, make the switch to
+ Tailwarden — our recommended cloud version for production use.{' '}
+
+
+
+ )}
>
);
}
diff --git a/dashboard/tailwind.config.js b/dashboard/tailwind.config.js
index 0744cc780..a90bf1875 100644
--- a/dashboard/tailwind.config.js
+++ b/dashboard/tailwind.config.js
@@ -172,7 +172,12 @@ module.exports = {
scale: 'scale 250ms ease forwards'
},
backgroundImage: {
- 'dependency-graph': 'radial-gradient(#EDEBEE 2px, transparent 0)'
+ 'dependency-graph': 'radial-gradient(#EDEBEE 2px, transparent 0)',
+ 'empty-cost-explorer':
+ "url('/assets/img/others/empty-state-cost-explorer.png')"
+ },
+ width: {
+ 'fit-content': 'fit-content'
}
}
},
diff --git a/dashboard/utils/providerHelper.ts b/dashboard/utils/providerHelper.ts
index 56676dc27..e4155e737 100644
--- a/dashboard/utils/providerHelper.ts
+++ b/dashboard/utils/providerHelper.ts
@@ -142,7 +142,6 @@ const platform: Platform = {
}
},
-
getImgSrc(providerName) {
const key = providerName.toLowerCase();
if (key in this.cloudProviders) return this.cloudProviders[key].imgSrc;