-
- {t('credentials.latest_requests')}
+
+
+ {t('credentials.latest_requests')}
+
+
+
void) => {
);
},
- enableSorting: false
+ enableSorting: false,
+ meta: {
+ pinned: 'right',
+ preventUnpinning: true
+ }
})
];
};
diff --git a/apps/frontend/components/composed/dashboard/DashboardContent.tsx b/apps/frontend/components/composed/dashboard/DashboardContent.tsx
index d667b4bb..e7febaf0 100644
--- a/apps/frontend/components/composed/dashboard/DashboardContent.tsx
+++ b/apps/frontend/components/composed/dashboard/DashboardContent.tsx
@@ -16,6 +16,7 @@ import { getStatusFilter } from '@utils/api/credentials/credentials.utils';
import { routes } from '@utils/routes';
import InfoCard from '@ui/InfoCard';
+import UpdateData from '@ui/UpdateData';
import PaginatedTable from '@ui/table/PaginatedTable';
import FilterMultiSelect from '@ui/table/filters/FilterMultiSelect';
import useClientSideMultiSelectFilter from '@ui/table/hooks/useClientSideMultiSelectFilter';
@@ -85,12 +86,14 @@ const DashboardContent = () => {
{t('credentials.latest_credentials')}
-
+
+
+
void;
+};
+
+const UpdateData = ({ onRefetch }: TUpdateData) => {
+ const [isAnimating, setIsAnimating] = useState(false);
+ const [startTime, setStartTime] = useState(Date.now());
+
+ const timeElapsed = useTimeElapsed(new Date(startTime));
+
+ const t = useTranslations();
+
+ const animate = () => {
+ setIsAnimating(true);
+ setTimeout(() => {
+ setIsAnimating(false);
+ }, 1000);
+ };
+
+ const handleRefetch = () => {
+ onRefetch();
+ setStartTime(Date.now());
+
+ animate();
+ };
+
+ return (
+
+
+ {timeElapsed && (
+ <>
+ {t('global.updated_time_ago', { time: timeElapsed })}
+ {t('global.updated_tooltip')}
}>
+
+
+ >
+ )}
+
+
+
+ );
+};
+
+export default UpdateData;
diff --git a/apps/frontend/components/ui/tooltip/Tooltip.components.tsx b/apps/frontend/components/ui/tooltip/Tooltip.components.tsx
new file mode 100644
index 00000000..66ba06a0
--- /dev/null
+++ b/apps/frontend/components/ui/tooltip/Tooltip.components.tsx
@@ -0,0 +1,49 @@
+import * as TooltipPrimitive from '@radix-ui/react-tooltip';
+import type { ComponentPropsWithoutRef, ElementRef } from 'react';
+import { forwardRef } from 'react';
+
+import { cn } from '@utils/cn';
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipPortal = TooltipPrimitive.Portal;
+
+const TooltipArrow = forwardRef<
+ ElementRef
,
+ ComponentPropsWithoutRef
+>((props, ref) => (
+
+));
+
+TooltipArrow.displayName = 'TooltipArrow';
+
+const TooltipContent = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, sideOffset = 2, ...props }, ref) => (
+
+));
+
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export {
+ Tooltip as TooltipRoot,
+ TooltipTrigger,
+ TooltipPortal,
+ TooltipContent,
+ TooltipProvider,
+ TooltipArrow
+};
diff --git a/apps/frontend/components/ui/tooltip/Tooltip.tsx b/apps/frontend/components/ui/tooltip/Tooltip.tsx
new file mode 100644
index 00000000..81ea24a9
--- /dev/null
+++ b/apps/frontend/components/ui/tooltip/Tooltip.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import type { FC, ReactNode } from 'react';
+
+import * as Tooltip from '@components/ui/tooltip/Tooltip.components';
+
+type TooltipProps = {
+ content: ReactNode;
+ children: ReactNode;
+};
+
+const TooltipComponent: FC = ({
+ children,
+ content,
+ ...triggerProps
+}) => (
+
+
+
+ {children}
+
+
+ {content}
+
+
+
+
+);
+
+export default TooltipComponent;
diff --git a/apps/frontend/messages/en.json b/apps/frontend/messages/en.json
index e36d040e..ed4dda0c 100644
--- a/apps/frontend/messages/en.json
+++ b/apps/frontend/messages/en.json
@@ -2,7 +2,13 @@
"global": {
"n_a": "N/A",
"filter_by": "Filter by",
- "error_message": "Seems like something went wrong..."
+ "error_message": "Seems like something went wrong...",
+ "update_data": "Update data",
+ "updated_time_ago": "Updated {time} ago",
+ "updated_tooltip": "We make sure to fetch the data at least once per day to keep everything running smoothly.",
+ "less_one_minute": "less than one minute",
+ "minutes_unit": "m",
+ "hours_unit": "h"
},
"navigation": {
"title": "Enterprise",
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 744cffd3..cc4856d5 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -22,6 +22,7 @@
"@radix-ui/react-select": "2.0.0",
"@radix-ui/react-slider": "1.1.2",
"@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-tooltip": "1.0.7",
"@tanstack/react-query": "5.18.1",
"@tanstack/react-table": "8.11.8",
"axios": "1.6.5",
diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml
index b19929cf..87d28f83 100644
--- a/apps/frontend/pnpm-lock.yaml
+++ b/apps/frontend/pnpm-lock.yaml
@@ -35,6 +35,9 @@ dependencies:
'@radix-ui/react-slot':
specifier: 1.0.2
version: 1.0.2(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-tooltip':
+ specifier: 1.0.7
+ version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: 5.18.1
version: 5.18.1(react@18.2.0)
@@ -1103,6 +1106,38 @@ packages:
react: 18.2.0
dev: false
+ /@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.23.8
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-id': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.47)(react@18.2.0)
+ '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
+ '@types/react': 18.2.47
+ '@types/react-dom': 18.2.18
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.47)(react@18.2.0):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
diff --git a/apps/frontend/tailwind.config.ts b/apps/frontend/tailwind.config.ts
index 4215731d..e6f3f88b 100644
--- a/apps/frontend/tailwind.config.ts
+++ b/apps/frontend/tailwind.config.ts
@@ -113,7 +113,9 @@ const config: Config = {
'radix-collapse-slide-up': 'radix-collapse-slide-up 150ms ease-out',
'spin-infinite': 'spin 2s linear infinite',
'accordion-down': 'accordion-down 0.2s ease-out',
- 'accordion-up': 'accordion-up 0.2s ease-out'
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ 'fade-in': 'fade-in 150ms ease-out',
+ 'fade-out': 'fade-out 150ms ease-out'
},
keyframes: {
spin: {
@@ -143,6 +145,14 @@ const config: Config = {
to: {
height: '0'
}
+ },
+ 'fade-in': {
+ '0%': { opacity: '0' },
+ '100%': { opacity: '1' }
+ },
+ 'fade-out': {
+ '0%': { opacity: '1' },
+ '100%': { opacity: '0' }
}
}
}
diff --git a/apps/frontend/utils/hooks/useTimeElapsed.ts b/apps/frontend/utils/hooks/useTimeElapsed.ts
new file mode 100644
index 00000000..60ade747
--- /dev/null
+++ b/apps/frontend/utils/hooks/useTimeElapsed.ts
@@ -0,0 +1,39 @@
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+
+const ONE_SECOND = 1000;
+
+const useTimeElapsed = (startTime: Date): string => {
+ const [timeElapsed, setTimeElapsed] = useState('');
+
+ const t = useTranslations();
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const currentTime = new Date();
+ const difference = Math.abs(currentTime.getTime() - startTime.getTime());
+ const secondsElapsed = Math.floor(difference / 1000);
+
+ const m = t('global.minutes_unit');
+ const h = t('global.hours_unit');
+
+ if (secondsElapsed < 60) {
+ setTimeElapsed(t('global.less_one_minute'));
+ } else {
+ const minutesElapsed = Math.floor(secondsElapsed / 60);
+ if (minutesElapsed < 60) {
+ setTimeElapsed(`${minutesElapsed}${m}`);
+ } else {
+ const hoursElapsed = Math.floor(minutesElapsed / 60);
+ setTimeElapsed(`${hoursElapsed}${h}`);
+ }
+ }
+ }, ONE_SECOND);
+
+ return () => clearInterval(interval);
+ }, [startTime, t]);
+
+ return timeElapsed;
+};
+
+export default useTimeElapsed;