diff --git a/apps/frontend/components/composed/credentials/overall/list/OverallContent.tsx b/apps/frontend/components/composed/credentials/overall/list/OverallContent.tsx index 7036b6e1..3d3c69de 100644 --- a/apps/frontend/components/composed/credentials/overall/list/OverallContent.tsx +++ b/apps/frontend/components/composed/credentials/overall/list/OverallContent.tsx @@ -10,6 +10,7 @@ import { StatusOptions } from '@utils/api/credentials/credentials.type'; import { getStatusFilter } from '@utils/api/credentials/credentials.utils'; import { routes } from '@utils/routes'; +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'; @@ -51,12 +52,13 @@ const OverallContent = () => {
{t('credentials.overall_requests')}
-
+
+
{ return (
-
- {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;