diff --git a/package-lock.json b/package-lock.json index d7255fb..93659d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,13 +29,17 @@ "lucide-react": "^0.303.0", "next": "14.0.4", "next-themes": "^0.2.1", + "oxide.ts": "^1.1.0", "postcss": "8.4.31", "react": "18.2.0", "react-dom": "18.2.0", + "secure-string": "^1.2.1", + "swr": "^2.2.4", "tailwind-merge": "^2.2.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", "typescript": "5.2.2", + "zod": "^3.22.4", "zustand": "^4.4.7" }, "devDependencies": { @@ -8376,6 +8380,11 @@ "node": ">= 0.8.0" } }, + "node_modules/oxide.ts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/oxide.ts/-/oxide.ts-1.1.0.tgz", + "integrity": "sha512-+MkqFRQVHEe/x4/cJ6KuYz2m2VpnoBi7aKLbttGYTxmpNZalQ2RbKH2HxyfsTqXJhjh9DYxulPWfQV/hWMmzCg==" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9318,6 +9327,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/secure-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/secure-string/-/secure-string-1.2.1.tgz", + "integrity": "sha512-hLvUbrSDGtM4TwB+xCA2ODc5m+PYiSRreXmosq8Dav33pk4qJ4M2KeDctj7Roj+PcIcP1V1Gwxu5AVtlPCO2PQ==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9713,6 +9727,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz", + "integrity": "sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -10639,6 +10665,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zustand": { "version": "4.4.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", diff --git a/package.json b/package.json index 4e21e1e..5661ca0 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,17 @@ "lucide-react": "^0.303.0", "next": "14.0.4", "next-themes": "^0.2.1", + "oxide.ts": "^1.1.0", "postcss": "8.4.31", "react": "18.2.0", "react-dom": "18.2.0", + "secure-string": "^1.2.1", + "swr": "^2.2.4", "tailwind-merge": "^2.2.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", "typescript": "5.2.2", + "zod": "^3.22.4", "zustand": "^4.4.7" }, "devDependencies": { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 33b2db5..26bd3fd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import React, { ReactNode } from 'react'; import Sidebar from '@/sections/dashboard/components/sidebar/sidebar'; import Header from '@/sections/dashboard/components/header/header'; import { ThemeProvider } from '@/sections/dashboard/components/header/theme-provider'; +import { ProfileProvider } from '@/sections/dashboard/context/profile-context'; export const metadata: Metadata = { title: 'Create Next App', @@ -24,11 +25,13 @@ export default function RootLayout({ children }: Readonly) { enableSystem disableTransitionOnChange > -
-
- -
{children}
-
+ +
+
+ +
{children}
+
+ diff --git a/src/sections/dashboard/components/header/profile-nav.tsx b/src/sections/dashboard/components/header/profile-nav.tsx index 24d71d1..b10732f 100644 --- a/src/sections/dashboard/components/header/profile-nav.tsx +++ b/src/sections/dashboard/components/header/profile-nav.tsx @@ -9,30 +9,33 @@ import { } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { ChevronDown, ChevronUp } from 'lucide-react'; +import { useProfileContext } from '@/sections/dashboard/context/profile-context'; +import { z } from 'zod'; // TODO: -// - Load available profiles // - Truncate profile names longer than 4 characters -// - Switch profile on click in dropdown -// - Pick default profile as default +// - Handle errors interface ProfileNavItemProps { profileName: string; accessKeyId: string; region: string; + onClick: () => void; } export const ProfileNavItem: React.FC = ({ profileName, accessKeyId, region, + onClick, }) => ( + {open ? ( + profileSet && Object.keys(profileSet.profiles).length > 1 ? ( + + ) : ( + <> + ) + ) : profileSet && Object.keys(profileSet.profiles).length > 1 ? ( + + ) : ( + <> + )} + + )} - - + {profileSet && + Object.entries(profileSet?.profiles) + .filter(([profile]) => profile !== currentProfile) + .map(([profile, settings]) => ( + setCurrentProfile(profile)} + /> + ))} diff --git a/src/sections/dashboard/context/profile-context.tsx b/src/sections/dashboard/context/profile-context.tsx new file mode 100644 index 0000000..62450d3 --- /dev/null +++ b/src/sections/dashboard/context/profile-context.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React, { + createContext, + PropsWithChildren, + useContext, + useMemo, +} from 'react'; +import useSWR, { Fetcher } from 'swr'; +// see https://stackoverflow.com/a/77264549 +import { invoke } from '@tauri-apps/api/tauri'; + +type ProfileContextType = { + data: Record | undefined; + error: Error | undefined; + isLoading: boolean; +}; + +export const ProfileContext = createContext({ + data: undefined, + error: undefined, + isLoading: true, +}); + +export const ProfileProvider = ({ children }: PropsWithChildren) => { + const fetcher: Fetcher, string> = (cmd: string) => + invoke>(cmd); + const { data, error, isLoading } = useSWR, Error>( + 'get_profiles', + fetcher + ); + + const value = useMemo( + () => ({ data, error, isLoading }), + [data, error, isLoading] + ); + return ( + {children} + ); +}; + +export const useProfileContext = () => { + const context = useContext(ProfileContext); + + if (!context) { + throw new Error('useThemeContext must be used inside the ThemeProvider'); + } + + return context; +}; diff --git a/src/sections/dashboard/hooks/use-profile.tsx b/src/sections/dashboard/hooks/use-profile.tsx new file mode 100644 index 0000000..3f42b67 --- /dev/null +++ b/src/sections/dashboard/hooks/use-profile.tsx @@ -0,0 +1,18 @@ +'use client'; + +import useSWR, { Fetcher } from 'swr'; +import { invoke } from '@tauri-apps/api/tauri'; // see https://stackoverflow.com/a/77264549 + +import { ProfileSet } from '@/modules/profiles/profile'; +export const useProfile = () => { + const fetcher: Fetcher = (cmd: string) => + invoke(cmd); + + const { + data: profileSet, + error, + isLoading, + } = useSWR('get_profiles', fetcher); + + return { profileSet, error, isLoading }; +}; diff --git a/tsconfig.json b/tsconfig.json index 198314c..c00f458 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ ], "paths": { "@/*": ["./src/*"] - } + }, + "typeRoots": ["./node_modules/@types", "./types"] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules", "src-tauri"] diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..dbd4082 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,3 @@ +declare module 'secure-string' { + export declare class SecureString {} +}