diff --git a/.env b/.env index 3b2f823..0ba4b92 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -"VITE_BACKEND_PORT=9000" +VITE_BACKEND_PORT=9000 +VITE_API_TIMEOUT=5000 diff --git a/package-lock.json b/package-lock.json index 13b7546..6246795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,11 +25,14 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^1.0.0", + "dotenv": "^16.4.5", "lucide-react": "^0.371.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-router-dom": "^6.16.0", "react-wrap-balancer": "^1.1.0", + "sonner": "^1.4.41", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7" }, @@ -4341,6 +4344,14 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "license": "MIT", @@ -4366,6 +4377,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browserslist": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", @@ -4845,6 +4871,11 @@ "node": ">=6" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, "node_modules/detect-node-es": { "version": "1.1.0", "license": "MIT" @@ -4881,6 +4912,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.748", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.748.tgz", @@ -7564,6 +7606,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7762,6 +7809,15 @@ "node": ">=12" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "license": "MIT", @@ -7780,6 +7836,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7832,6 +7893,14 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -8012,6 +8081,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/once": { "version": "1.4.0", "license": "ISC", @@ -8383,6 +8457,31 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "dev": true, @@ -8615,6 +8714,11 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8669,7 +8773,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8846,6 +8949,15 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "1.4.41", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", + "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -9361,6 +9473,15 @@ "node": ">=4" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "dev": true, diff --git a/package.json b/package.json index 9eec9b0..77bd14a 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,14 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^1.0.0", + "dotenv": "^16.4.5", "lucide-react": "^0.371.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-router-dom": "^6.16.0", "react-wrap-balancer": "^1.1.0", + "sonner": "^1.4.41", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7" }, diff --git a/src/App.tsx b/src/App.tsx index 654103c..7087140 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,13 +2,30 @@ import { RouterProvider } from "react-router-dom"; import { ThemeProvider } from "./contexts/ThemeContext"; import { router } from "./Router"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { toast, Toaster } from "sonner"; +import { QueryClient, QueryClientProvider } from "react-query"; export default function App() { + const queryClient = new QueryClient(); + queryClient.setDefaultOptions({ + queries: { + onError: (error: unknown) => { + if (error instanceof Error) { + toast.error(`Something went wrong: ${error.message}`); + } + }, + staleTime: 10000, + refetchOnWindowFocus: "always", + }, + }); return ( - - - + + + + + + ); } diff --git a/src/Router.tsx b/src/Router.tsx index 60087d7..c745a92 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -6,7 +6,7 @@ import NoMatch from "./modules/fallback/NoMatch"; import ClustersPage from "@/modules/clusters/clusters-list/ClustersPage"; import { appConfig } from "@/config/app"; import { ClusterInfo } from "@/modules/clusters/cluster-information/ClusterInfo"; -const defaultTab = appConfig.sveltosType; +const defaultTab = appConfig.defaultType; const defaultPage = appConfig.defaultPage; export const router = createBrowserRouter( [ diff --git a/src/api-client/apiClient.ts b/src/api-client/apiClient.ts index 06f5f17..12ef360 100644 --- a/src/api-client/apiClient.ts +++ b/src/api-client/apiClient.ts @@ -1,6 +1,14 @@ import axios from "axios"; +import { toast } from "sonner"; const client = axios.create({ - baseURL: import.meta.env.VITE_SVELTOS_API_BASE_URL, - timeout: 1000, + baseURL: "/api", + timeout: import.meta.env.VITE_API_TIMEOUT, }); + +client.interceptors.request.use(function (config) { + config.headers["Content-Type"] = "application/json"; + return config; +}); + +export default client; diff --git a/src/components/layouts/Header.tsx b/src/components/layouts/Header.tsx index c868c3c..bdd5df6 100644 --- a/src/components/layouts/Header.tsx +++ b/src/components/layouts/Header.tsx @@ -29,7 +29,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { ModeToggle } from "@/components/mode-toggle"; -import { Badge } from "@/components/ui/badge"; + export function Header() { const [open, setOpen] = useState(false); @@ -99,7 +99,7 @@ export function Header() { cn( "text-sm font-medium flex items-center transition-colors hover:text-primary", isActive - ? "bg-slate-100 dark:bg-slate-700 p-2 rounded hover:text-main-500 " + ? "bg-slate-100 dark:bg-slate-800 p-2 rounded hover:text-main-500 " : "text-foreground/60 hover:text-primary", ) } diff --git a/src/components/ui/PageHeading.tsx b/src/components/ui/PageHeading.tsx index 0ef4de8..99ffad9 100644 --- a/src/components/ui/PageHeading.tsx +++ b/src/components/ui/PageHeading.tsx @@ -2,6 +2,7 @@ import { CardsFilterToolbar } from "@/modules/clusters/clusters-list/components/ interface PageHeadingProps { title: string; description: string; + } export const PageHeading = ({ title, description }: PageHeadingProps) => { return ( @@ -12,10 +13,6 @@ export const PageHeading = ({ title, description }: PageHeadingProps) => {

{title}

{description} - - Retry - {" "} - or{" "} ( + ({ className, variant, size, asChild = false, ...props }, ref) => { + + const queryClient = useQueryClient(); + const isFetching = useIsFetching(); + const handleRefresh = () =>{ + queryClient.refetchQueries(); + }; + return ( + + ); + }, +); +RefreshButton.displayName = "Button"; + +export { RefreshButton }; \ No newline at end of file diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..13219e7 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +

+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index d3d5d60..4d49336 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -8,6 +8,8 @@ const badgeVariants = cva( { variants: { variant: { + label: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: @@ -28,8 +30,10 @@ export interface BadgeProps VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { + const isLabelVariant = variant === 'label'; + return ( -
+
); } diff --git a/src/components/ui/emptyData.tsx b/src/components/ui/emptyData.tsx new file mode 100644 index 0000000..d5094f7 --- /dev/null +++ b/src/components/ui/emptyData.tsx @@ -0,0 +1,51 @@ + +import { CircleOff, FilterX, RefreshCcw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useQueryClient } from "react-query"; +import { RefreshButton } from "@/components/ui/RefreshButton"; + +type EmptyDataProps = { + name: string; + isFiltered?: boolean; + clearFilter?: () => void; + refreshKey?: string; +} + +export const EmptyData = ({name,isFiltered,clearFilter,refreshKey}:EmptyDataProps) => { + const queryClient = useQueryClient(); + const handleRefresh = () => { + + queryClient.invalidateQueries() + + } + return ( + <> +
+
+
+ +
+
+

+ There’s no {name} here +

+

+ {isFiltered + ? "Try changing the filter criteria or clear the filter" + : "Try refreshing the page or check back later"} + +

+
+ {isFiltered && ( + + )} + +
+
+
+
+ + ); +}; diff --git a/src/components/ui/errorQuery.tsx b/src/components/ui/errorQuery.tsx new file mode 100644 index 0000000..f183d74 --- /dev/null +++ b/src/components/ui/errorQuery.tsx @@ -0,0 +1,55 @@ +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; + +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { AlertTriangleIcon, RefreshCcw } from "lucide-react"; +import { appConfig } from "@/config/app"; +import { useQueryClient } from "react-query"; +import { RefreshButton } from "@/components/ui/RefreshButton"; + +type ErrorFetchingProps = { + name: string; + error: unknown; + queryKey?: string; +}; + +export const ErrorQuery = ({ name, error, queryKey }: ErrorFetchingProps) => { + + const reportError = () => { + window.open(appConfig.github.url + "/issues/new", "_blank"); + }; + + return ( + <> + + + Error + +
+ + Failed to retrieve {name}, please ensure the backend is running on + port {import.meta.env.VITE_BACKEND_PORT}. + + +
+ + +
+
+ {error instanceof Error && ( +

{error.message}

+ )} +
+
+
+ + ); +}; diff --git a/src/components/ui/failed-flag.tsx b/src/components/ui/failed-flag.tsx index 8f3df55..c2c0d47 100644 --- a/src/components/ui/failed-flag.tsx +++ b/src/components/ui/failed-flag.tsx @@ -10,7 +10,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -export const FailedFlag = () => { +export const FailedFlag = ({ msg }: { msg?: string | undefined | null }) => { return ( <> @@ -25,6 +25,7 @@ export const FailedFlag = () => {

Unhealthy

+ {msg &&

{msg}

}
diff --git a/src/config/app.ts b/src/config/app.ts index c3d2244..d103b5e 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -1,14 +1,20 @@ -export type clusterType = "SveltosCluster" | "ClusterAPI"; +import { + ClusterApiType, + ClusterType, + SveltosClusterType, +} from "@/types/cluster"; +import { sveltosClusterValue } from "@/types/cluster.consts"; + interface AppConfig { name: string; github: { title: string; url: string; }; - sveltosType: clusterType; - clusterAPIType: clusterType; - defaultType: clusterType; + defaultType: ClusterType; defaultPage: number; + defaultSize: number; + maxBadges: number; } export const appConfig: AppConfig = { @@ -17,8 +23,8 @@ export const appConfig: AppConfig = { title: "dashboard", url: "https://github.com/projectsveltos/dashboard", }, - sveltosType: "SveltosCluster", - clusterAPIType: "ClusterAPI", - defaultType: "SveltosCluster", + defaultType: sveltosClusterValue, defaultPage: 0, + defaultSize: 8, + maxBadges: 2, }; diff --git a/src/modules/clusters/clusters-list/ClustersPage.tsx b/src/modules/clusters/clusters-list/ClustersPage.tsx index 571420b..8bcc506 100644 --- a/src/modules/clusters/clusters-list/ClustersPage.tsx +++ b/src/modules/clusters/clusters-list/ClustersPage.tsx @@ -1,162 +1,39 @@ import { PageHeading } from "@/components/ui/PageHeading"; -import { appConfig, clusterType } from "@/config/app"; +import { appConfig } from "@/config/app"; import { useNavigate, useParams } from "react-router-dom"; import { useState } from "react"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ClusterType } from "@/types/cluster"; +import useClusters from "@/modules/clusters/clusters-list/hooks/useClusters"; +import { clusterTypes } from "@/types/cluster.consts"; import { ClusterList } from "@/modules/clusters/clusters-list/components/ClusterList"; +import { ErrorQuery } from "@/components/ui/errorQuery"; +import { LoadingCards } from "@/modules/clusters/clusters-list/components/LoadingCards"; + export default function ClustersPage() { - const dummyClusterData = [ - { - name: "Cluster 1", - version: "v1.22.3", - namespace: "namespace1", - type: "clusterAPI", - status: true, - labels: [ - { - designation: "env:production", - color: "red", - }, - { - designation: "euw:devp1", - color: "green", - }, - ], - }, - { - name: "Cluster 2", - version: "v1.20.7", - type: "sveltosCluster", - namespace: "namespace2", - status: true, - labels: [ - { - designation: "ru:devp2", - color: "amber", - }, - { - designation: "env:engi", - color: "purple", - }, - ], - }, - { - name: "Cluster 2", - version: "v1.20.7", - type: "clusterAPI", - namespace: "namespace2", - status: false, - labels: [ - { - designation: "weu:development", - color: "yellow", - }, - { - designation: "env:eng", - color: "purple", - }, - ], - }, - { - name: "Cluster 2", - version: "v1.20.7", - type: "clusterAPI", - namespace: "namespace2", - status: true, - labels: [ - { - designation: "env:development", - color: "yellow", - }, - { - designation: "euw:engineering", - color: "purple", - }, - ], - }, - { - name: "Cluster 2", - version: "v1.20.7", - type: "sveltosCluster", - namespace: "namespace2", - status: true, - labels: [ - { - designation: "env:development", - color: "yellow", - }, - { - designation: "env:eng", - color: "purple", - }, - ], - }, - { - name: "Cluster 2", - type: "sveltosCluster", - version: "v1.20.7", - namespace: "namespace2", - status: false, - labels: [ - { - designation: "euw:development", - color: "yellow", - }, - { - designation: "euw:engineering", - color: "purple", - }, - ], - }, - { - name: "Cluster 2", - version: "v1.20.7", - type: "clusterAPI", - namespace: "namespace2", - status: false, - labels: [ - { - designation: "euw:par1", - color: "amber", - }, - { - designation: "euw:par2", - color: "purple", - }, - ], - }, - { - name: "Cluster 78", - version: "v1.21.5", - type: "sveltosCluster", - namespace: "namespace3", - status: true, - labels: [ - { - designation: "env:staging", - color: "orange", - }, - { - designation: "na:qa", - color: "red", - }, - ], - }, - ]; const navigate = useNavigate(); const defaultTab = appConfig.defaultType; const defaultPage = appConfig.defaultPage; const { tab: urlTab, page: urlPage } = useParams(); - const [currentTab, setCurrentTab] = useState(() => { - return urlTab ? (urlTab as clusterType) : defaultTab; + const [currentTab, setCurrentTab] = useState(() => { + return urlTab ? (urlTab as ClusterType) : defaultTab; }); const [currentPage, setCurrentPage] = useState(() => { return urlPage ? parseInt(urlPage) : defaultPage; }); - - const handleTabChange = (value: clusterType) => { + const { + data = [], + isLoading, + isError, + isSuccess, + error, + refetch, + } = useClusters(currentTab); + const handleTabChange = (value: ClusterType) => { + setCurrentTab(value); navigate(`/clusters/${value}/${currentPage}`); }; @@ -165,15 +42,26 @@ export default function ClustersPage() { - + + + {clusterTypes.map((type) => ( + handleTabChange(type)} + > + {type} + + ))} + + + {isLoading && } + {isError && } + {isSuccess && } ); } diff --git a/src/modules/clusters/clusters-list/components/CardsFilterToolbar.tsx b/src/modules/clusters/clusters-list/components/CardsFilterToolbar.tsx index 149d2a0..9b3f97f 100644 --- a/src/modules/clusters/clusters-list/components/CardsFilterToolbar.tsx +++ b/src/modules/clusters/clusters-list/components/CardsFilterToolbar.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Cross2Icon } from "@radix-ui/react-icons"; import { useState } from "react"; import { CardsFacetedFilter } from "@/modules/clusters/clusters-list/components/CardsFacetedFilter"; +import { RefreshButton } from "@/components/ui/RefreshButton"; export const CardsFilterToolbar = () => { const [isFiltered, setIsFiltered] = useState(false); @@ -31,6 +32,7 @@ export const CardsFilterToolbar = () => { { label: "Inactive", value: "inactive" }, ]} /> + {isFiltered && (