diff --git a/apps/webui/graphql-codegen.ts b/apps/webui/graphql-codegen.ts
index f0112eb..4c60969 100644
--- a/apps/webui/graphql-codegen.ts
+++ b/apps/webui/graphql-codegen.ts
@@ -10,6 +10,9 @@ const config: CodegenConfig = {
presetConfig: {
gqlTagName: 'gql',
},
+ config: {
+ enumsAsConst: true,
+ },
},
},
};
diff --git a/apps/webui/src/components/detail-card-skeleton.tsx b/apps/webui/src/components/detail-card-skeleton.tsx
new file mode 100644
index 0000000..0f44d96
--- /dev/null
+++ b/apps/webui/src/components/detail-card-skeleton.tsx
@@ -0,0 +1,32 @@
+import { Card, CardContent, CardHeader } from './ui/card';
+import { Skeleton } from './ui/skeleton';
+
+export function DetailCardSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/webui/src/components/layout/nav-main.tsx b/apps/webui/src/components/layout/nav-main.tsx
index 45209b0..1afa4a5 100644
--- a/apps/webui/src/components/layout/nav-main.tsx
+++ b/apps/webui/src/components/layout/nav-main.tsx
@@ -7,6 +7,14 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { ProLink, type ProLinkProps } from '@/components/ui/pro-link';
import {
SidebarGroup,
SidebarGroupLabel,
@@ -16,9 +24,9 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
+ useSidebar,
} from '@/components/ui/sidebar';
import { useMatches } from '@tanstack/react-router';
-import { ProLink, type ProLinkProps } from '../ui/pro-link';
export interface NavMainItem {
link?: ProLinkProps;
@@ -38,6 +46,7 @@ export function NavMain({
groups: NavMainGroup[];
}) {
const matches = useMatches();
+ const { state } = useSidebar();
const isMenuMatch = (link: ProLinkProps | undefined) => {
const linkTo = link?.to;
@@ -50,13 +59,84 @@ export function NavMain({
const renderSidebarMenuItemButton = (item: NavMainItem) => {
return (
<>
- {item.icon && }
- {item.title}
+ {item.icon && }
+ {item.title}
>
);
};
+ const renderCollapsedSubMenu = (item: NavMainItem) => {
+ return (
+
+
+
+ {item.icon && }
+
+
+
+
+ {item.title}
+
+
+ {item.children?.map((subItem, index) => (
+
+ {subItem.title}
+
+ ))}
+
+
+ );
+ };
+
+ const renderExpandedSubMenu = (item: NavMainItem, itemIndex: number) => {
+ return (
+
+
+
+
+ {renderSidebarMenuItemButton(item)}
+
+
+
+
+ {(item.children || []).map((subItem, subItemIndex) => {
+ return (
+
+
+
+ {subItem.title}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+ };
+
return groups.map((group, groupIndex) => {
return (
@@ -66,51 +146,21 @@ export function NavMain({
if (!item.children?.length) {
return (
-
-
+
+
{renderSidebarMenuItemButton(item)}
);
}
- return (
-
-
-
-
- {renderSidebarMenuItemButton(item)}
-
-
-
-
- {(item.children || []).map((subItem, subItemIndex) => {
- return (
-
-
-
- {subItem.title}
-
-
-
- );
- })}
-
-
-
-
- );
+ return state === 'collapsed'
+ ? renderCollapsedSubMenu(item)
+ : renderExpandedSubMenu(item, itemIndex);
})}
diff --git a/apps/webui/src/components/ui/detail-empty-view.tsx b/apps/webui/src/components/ui/detail-empty-view.tsx
new file mode 100644
index 0000000..66788d0
--- /dev/null
+++ b/apps/webui/src/components/ui/detail-empty-view.tsx
@@ -0,0 +1,18 @@
+import { memo } from "react";
+import { Card, CardContent } from "./card";
+
+export interface DetailEmptyViewProps {
+ message: string;
+}
+
+export const DetailEmptyView = memo(({ message }: DetailEmptyViewProps) => {
+ return (
+
+
+
+ {message ?? "No data"}
+
+
+
+ );
+});
diff --git a/apps/webui/src/components/ui/sidebar.tsx b/apps/webui/src/components/ui/sidebar.tsx
index 12dbc5c..6533a40 100644
--- a/apps/webui/src/components/ui/sidebar.tsx
+++ b/apps/webui/src/components/ui/sidebar.tsx
@@ -25,8 +25,6 @@ import {
import { useIsMobile } from "@/presentation/hooks/use-mobile";
import { cn } from "@/presentation/utils";
-const SIDEBAR_COOKIE_NAME = "sidebar_state";
-const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
@@ -81,10 +79,6 @@ function SidebarProvider({
} else {
_setOpen(openState);
}
-
- // This sets the cookie to keep the sidebar state.
- // TODO: FIX THIS
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
@@ -310,7 +304,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
0',
@@ -81,3 +99,10 @@ export type Credential3rdInsertDto = typeof Credential3rdInsertSchema.infer;
export type Credential3rdQueryDto =
GetCredential3rdQuery['credential3rd']['nodes'][number];
+
+export const Credential3rdUpdateSchema = Credential3rdInsertSchema.partial();
+
+export type Credential3rdUpdateDto = typeof Credential3rdUpdateSchema.infer;
+
+export type Credential3rdDetailDto =
+ GetCredential3rdDetailQuery['credential3rd']['nodes'][number];
diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts
index 8f09ab8..82ae093 100644
--- a/apps/webui/src/infra/graphql/gql/gql.ts
+++ b/apps/webui/src/infra/graphql/gql/gql.ts
@@ -18,6 +18,7 @@ type Documents = {
"\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.InsertCredential3rdDocument,
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": typeof types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": typeof types.DeleteCredential3rdDocument,
+ "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
"\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument,
@@ -29,6 +30,7 @@ const documents: Documents = {
"\n mutation InsertCredential3rd($data: Credential3rdInsertInput!) {\n credential3rdCreateOne(data: $data) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.InsertCredential3rdDocument,
"\n mutation UpdateCredential3rd($data: Credential3rdUpdateInput!, $filters: Credential3rdFilterInput!) {\n credential3rdUpdate(data: $data, filter: $filters) {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n": types.UpdateCredential3rdDocument,
"\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n": types.DeleteCredential3rdDocument,
+ "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
"\n query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument,
@@ -66,6 +68,10 @@ export function gql(source: "\n mutation UpdateCredential3rd($data: Credential3
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n"): (typeof documents)["\n mutation DeleteCredential3rd($filters: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filters)\n }\n"];
+/**
+ * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"): (typeof documents)["\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filters: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts
index 31d8a02..bf226ed 100644
--- a/apps/webui/src/infra/graphql/gql/graphql.ts
+++ b/apps/webui/src/infra/graphql/gql/graphql.ts
@@ -267,10 +267,11 @@ export type Credential3rdOrderInput = {
username?: InputMaybe;
};
-export enum Credential3rdTypeEnum {
- Mikan = 'mikan'
-}
+export const Credential3rdTypeEnum = {
+ Mikan: 'mikan'
+} as const;
+export type Credential3rdTypeEnum = typeof Credential3rdTypeEnum[keyof typeof Credential3rdTypeEnum];
export type Credential3rdTypeEnumFilterInput = {
eq?: InputMaybe;
gt?: InputMaybe;
@@ -300,11 +301,12 @@ export type CursorInput = {
limit: Scalars['Int']['input'];
};
-export enum DownloadMimeEnum {
- Applicationoctetstream = 'applicationoctetstream',
- Applicationxbittorrent = 'applicationxbittorrent'
-}
+export const DownloadMimeEnum = {
+ Applicationoctetstream: 'applicationoctetstream',
+ Applicationxbittorrent: 'applicationxbittorrent'
+} as const;
+export type DownloadMimeEnum = typeof DownloadMimeEnum[keyof typeof DownloadMimeEnum];
export type DownloadMimeEnumFilterInput = {
eq?: InputMaybe;
gt?: InputMaybe;
@@ -318,15 +320,16 @@ export type DownloadMimeEnumFilterInput = {
ne?: InputMaybe;
};
-export enum DownloadStatusEnum {
- Completed = 'completed',
- Deleted = 'deleted',
- Downloading = 'downloading',
- Failed = 'failed',
- Paused = 'paused',
- Pending = 'pending'
-}
+export const DownloadStatusEnum = {
+ Completed: 'completed',
+ Deleted: 'deleted',
+ Downloading: 'downloading',
+ Failed: 'failed',
+ Paused: 'paused',
+ Pending: 'pending'
+} as const;
+export type DownloadStatusEnum = typeof DownloadStatusEnum[keyof typeof DownloadStatusEnum];
export type DownloadStatusEnumFilterInput = {
eq?: InputMaybe;
gt?: InputMaybe;
@@ -340,11 +343,12 @@ export type DownloadStatusEnumFilterInput = {
ne?: InputMaybe;
};
-export enum DownloaderCategoryEnum {
- Dandanplay = 'dandanplay',
- Qbittorrent = 'qbittorrent'
-}
+export const DownloaderCategoryEnum = {
+ Dandanplay: 'dandanplay',
+ Qbittorrent: 'qbittorrent'
+} as const;
+export type DownloaderCategoryEnum = typeof DownloaderCategoryEnum[keyof typeof DownloaderCategoryEnum];
export type DownloaderCategoryEnumFilterInput = {
eq?: InputMaybe;
gt?: InputMaybe;
@@ -1007,11 +1011,12 @@ export type OffsetInput = {
offset: Scalars['Int']['input'];
};
-export enum OrderByEnum {
- Asc = 'ASC',
- Desc = 'DESC'
-}
+export const OrderByEnum = {
+ Asc: 'ASC',
+ Desc: 'DESC'
+} as const;
+export type OrderByEnum = typeof OrderByEnum[keyof typeof OrderByEnum];
export type PageInfo = {
__typename?: 'PageInfo';
endCursor?: Maybe;
@@ -1405,13 +1410,14 @@ export type SubscriptionBangumiUpdateInput = {
subscriptionId?: InputMaybe;
};
-export enum SubscriptionCategoryEnum {
- Manual = 'manual',
- MikanBangumi = 'mikan_bangumi',
- MikanSeason = 'mikan_season',
- MikanSubscriber = 'mikan_subscriber'
-}
+export const SubscriptionCategoryEnum = {
+ Manual: 'manual',
+ MikanBangumi: 'mikan_bangumi',
+ MikanSeason: 'mikan_season',
+ MikanSubscriber: 'mikan_subscriber'
+} as const;
+export type SubscriptionCategoryEnum = typeof SubscriptionCategoryEnum[keyof typeof SubscriptionCategoryEnum];
export type SubscriptionCategoryEnumFilterInput = {
eq?: InputMaybe;
gt?: InputMaybe;
@@ -1665,6 +1671,13 @@ export type DeleteCredential3rdMutationVariables = Exact<{
export type DeleteCredential3rdMutation = { __typename?: 'Mutation', credential3rdDelete: number };
+export type GetCredential3rdDetailQueryVariables = Exact<{
+ id: Scalars['Int']['input'];
+}>;
+
+
+export type GetCredential3rdDetailQuery = { __typename?: 'Query', credential3rd: { __typename?: 'Credential3rdConnection', nodes: Array<{ __typename?: 'Credential3rd', id: number, cookies?: string | null, username?: string | null, password?: string | null, userAgent?: string | null, createdAt: string, updatedAt: string, credentialType: Credential3rdTypeEnum }> } };
+
export type GetSubscriptionsQueryVariables = Exact<{
filters: SubscriptionsFilterInput;
orderBy: SubscriptionsOrderInput;
@@ -1708,6 +1721,7 @@ export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind
export const InsertCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode;
export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode;
+export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]}}]} as unknown as DocumentNode;
export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode;
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode;
diff --git a/apps/webui/src/infra/routes/nav.ts b/apps/webui/src/infra/routes/nav.ts
index 6d0a263..3180134 100644
--- a/apps/webui/src/infra/routes/nav.ts
+++ b/apps/webui/src/infra/routes/nav.ts
@@ -32,7 +32,7 @@ export const AppNavMainData = [
{
title: 'Subscriptions',
link: {
- to: '/subscriptions',
+ to: '/subscriptions/manage',
},
icon: Folders,
children: [
@@ -53,7 +53,7 @@ export const AppNavMainData = [
{
title: 'Credential',
link: {
- to: '/credential3rd',
+ to: '/credential3rd/manage',
},
icon: KeyRound,
children: [
diff --git a/apps/webui/src/infra/styles/context.ts b/apps/webui/src/infra/styles/context.ts
index 9c8bdb9..e5de700 100644
--- a/apps/webui/src/infra/styles/context.ts
+++ b/apps/webui/src/infra/styles/context.ts
@@ -37,8 +37,11 @@ export function useTheme() {
}, [injector]);
const colorTheme = useMemo(
- () => atomWithObservable(() => themeService.colorSchema$),
- [themeService]
+ () =>
+ atomWithObservable(() => themeService.colorSchema$, {
+ initialValue: themeService.colorSchema$.value,
+ }),
+ [themeService.colorSchema$]
);
return {
diff --git a/apps/webui/src/infra/styles/theme.service.ts b/apps/webui/src/infra/styles/theme.service.ts
index 8d3810e..00c60d8 100644
--- a/apps/webui/src/infra/styles/theme.service.ts
+++ b/apps/webui/src/infra/styles/theme.service.ts
@@ -3,18 +3,17 @@ import { LocalStorageService } from '@/infra/storage/web-storage.service';
import { Injectable, inject } from '@outposts/injection-js';
import {
BehaviorSubject,
- ReplaySubject,
combineLatest,
distinctUntilChanged,
filter,
fromEvent,
map,
- shareReplay,
- startWith,
} from 'rxjs';
export type PreferColorSchemaType = 'dark' | 'light' | 'system';
export type PreferColorSchemaClass = 'dark' | 'light';
+const MOBILE_BREAKPOINT = 768;
+
@Injectable()
export class ThemeService {
document = inject(DOCUMENT);
@@ -29,17 +28,34 @@ export class ThemeService {
this.systemColorSchema$.value
)
);
+ isMobile$ = new BehaviorSubject(
+ this.getIsMobileByInnerWidth(this.document.defaultView?.innerWidth)
+ );
setup() {
- const mediaQuery = this.document.defaultView?.matchMedia(
+ const isMobileMediaQuery = this.document.defaultView?.matchMedia(
+ `(max-width: ${MOBILE_BREAKPOINT - 1}px)`
+ );
+
+ if (isMobileMediaQuery) {
+ fromEvent(isMobileMediaQuery, 'change')
+ .pipe(
+ map(() =>
+ this.getIsMobileByInnerWidth(this.document.defaultView?.innerWidth)
+ ),
+ distinctUntilChanged()
+ )
+ .subscribe(this.isMobile$);
+ }
+
+ const systemColorSchemaMediaQuery = this.document.defaultView?.matchMedia(
'(prefers-color-scheme: dark)'
);
- if (mediaQuery) {
- fromEvent(mediaQuery, 'change')
+ if (systemColorSchemaMediaQuery) {
+ fromEvent(systemColorSchemaMediaQuery, 'change')
.pipe(
- map(() => (mediaQuery.matches ? 'dark' : 'light')),
- startWith(this.systemColorSchema),
+ map(() => (systemColorSchemaMediaQuery.matches ? 'dark' : 'light')),
distinctUntilChanged()
)
.subscribe(this.systemColorSchema$);
@@ -85,6 +101,13 @@ export class ThemeService {
return systemColorSchema;
}
+ private getIsMobileByInnerWidth(innerWidth: number | undefined): boolean {
+ if (innerWidth === undefined) {
+ return false;
+ }
+ return innerWidth < MOBILE_BREAKPOINT;
+ }
+
get systemColorSchema(): PreferColorSchemaClass {
return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)')
.matches
diff --git a/apps/webui/src/presentation/hooks/use-mobile.ts b/apps/webui/src/presentation/hooks/use-mobile.ts
index 2b0fe1d..18b396b 100644
--- a/apps/webui/src/presentation/hooks/use-mobile.ts
+++ b/apps/webui/src/presentation/hooks/use-mobile.ts
@@ -1,19 +1,21 @@
-import * as React from "react"
-
-const MOBILE_BREAKPOINT = 768
+import { useInject } from '@/infra/di/inject';
+import { ThemeService } from '@/infra/styles/theme.service';
+import { useAtomValue } from 'jotai/react';
+import { atomWithObservable } from 'jotai/utils';
+import { useMemo } from 'react';
export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState(undefined)
+ const themeService = useInject(ThemeService);
- React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
- const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
+ const isMobile = useAtomValue(
+ useMemo(
+ () =>
+ atomWithObservable(() => themeService.isMobile$, {
+ initialValue: themeService.isMobile$.value,
+ }),
+ [themeService.isMobile$]
+ )
+ );
- return !!isMobile
+ return isMobile;
}
diff --git a/apps/webui/src/presentation/routeTree.gen.ts b/apps/webui/src/presentation/routeTree.gen.ts
index b2971c1..6964d4f 100644
--- a/apps/webui/src/presentation/routeTree.gen.ts
+++ b/apps/webui/src/presentation/routeTree.gen.ts
@@ -34,6 +34,7 @@ import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed'
import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore'
import { Route as AppSubscriptionsEditSubscriptionIdImport } from './routes/_app/subscriptions/edit.$subscriptionId'
import { Route as AppSubscriptionsDetailSubscriptionIdImport } from './routes/_app/subscriptions/detail.$subscriptionId'
+import { Route as AppCredential3rdEditIdImport } from './routes/_app/credential3rd/edit.$id'
import { Route as AppCredential3rdDetailIdImport } from './routes/_app/credential3rd/detail.$id'
// Create/Update Routes
@@ -179,6 +180,12 @@ const AppSubscriptionsDetailSubscriptionIdRoute =
getParentRoute: () => AppSubscriptionsRouteRoute,
} as any)
+const AppCredential3rdEditIdRoute = AppCredential3rdEditIdImport.update({
+ id: '/edit/$id',
+ path: '/edit/$id',
+ getParentRoute: () => AppCredential3rdRouteRoute,
+} as any)
+
const AppCredential3rdDetailIdRoute = AppCredential3rdDetailIdImport.update({
id: '/detail/$id',
path: '/detail/$id',
@@ -343,6 +350,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppCredential3rdDetailIdImport
parentRoute: typeof AppCredential3rdRouteImport
}
+ '/_app/credential3rd/edit/$id': {
+ id: '/_app/credential3rd/edit/$id'
+ path: '/edit/$id'
+ fullPath: '/credential3rd/edit/$id'
+ preLoaderRoute: typeof AppCredential3rdEditIdImport
+ parentRoute: typeof AppCredential3rdRouteImport
+ }
'/_app/subscriptions/detail/$subscriptionId': {
id: '/_app/subscriptions/detail/$subscriptionId'
path: '/detail/$subscriptionId'
@@ -378,12 +392,14 @@ interface AppCredential3rdRouteRouteChildren {
AppCredential3rdCreateRoute: typeof AppCredential3rdCreateRoute
AppCredential3rdManageRoute: typeof AppCredential3rdManageRoute
AppCredential3rdDetailIdRoute: typeof AppCredential3rdDetailIdRoute
+ AppCredential3rdEditIdRoute: typeof AppCredential3rdEditIdRoute
}
const AppCredential3rdRouteRouteChildren: AppCredential3rdRouteRouteChildren = {
AppCredential3rdCreateRoute: AppCredential3rdCreateRoute,
AppCredential3rdManageRoute: AppCredential3rdManageRoute,
AppCredential3rdDetailIdRoute: AppCredential3rdDetailIdRoute,
+ AppCredential3rdEditIdRoute: AppCredential3rdEditIdRoute,
}
const AppCredential3rdRouteRouteWithChildren =
@@ -481,6 +497,7 @@ export interface FileRoutesByFullPath {
'/subscriptions/manage': typeof AppSubscriptionsManageRoute
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
+ '/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
}
@@ -508,6 +525,7 @@ export interface FileRoutesByTo {
'/subscriptions/manage': typeof AppSubscriptionsManageRoute
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
+ '/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
}
@@ -536,6 +554,7 @@ export interface FileRoutesById {
'/_app/subscriptions/manage': typeof AppSubscriptionsManageRoute
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/_app/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
+ '/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/_app/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/_app/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
}
@@ -565,6 +584,7 @@ export interface FileRouteTypes {
| '/subscriptions/manage'
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
+ | '/credential3rd/edit/$id'
| '/subscriptions/detail/$subscriptionId'
| '/subscriptions/edit/$subscriptionId'
fileRoutesByTo: FileRoutesByTo
@@ -591,6 +611,7 @@ export interface FileRouteTypes {
| '/subscriptions/manage'
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
+ | '/credential3rd/edit/$id'
| '/subscriptions/detail/$subscriptionId'
| '/subscriptions/edit/$subscriptionId'
id:
@@ -617,6 +638,7 @@ export interface FileRouteTypes {
| '/_app/subscriptions/manage'
| '/auth/oidc/callback'
| '/_app/credential3rd/detail/$id'
+ | '/_app/credential3rd/edit/$id'
| '/_app/subscriptions/detail/$subscriptionId'
| '/_app/subscriptions/edit/$subscriptionId'
fileRoutesById: FileRoutesById
@@ -695,7 +717,8 @@ export const routeTree = rootRoute
"children": [
"/_app/credential3rd/create",
"/_app/credential3rd/manage",
- "/_app/credential3rd/detail/$id"
+ "/_app/credential3rd/detail/$id",
+ "/_app/credential3rd/edit/$id"
]
},
"/_app/playground": {
@@ -771,6 +794,10 @@ export const routeTree = rootRoute
"filePath": "_app/credential3rd/detail.$id.tsx",
"parent": "/_app/credential3rd"
},
+ "/_app/credential3rd/edit/$id": {
+ "filePath": "_app/credential3rd/edit.$id.tsx",
+ "parent": "/_app/credential3rd"
+ },
"/_app/subscriptions/detail/$subscriptionId": {
"filePath": "_app/subscriptions/detail.$subscriptionId.tsx",
"parent": "/_app/subscriptions"
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
index 870b968..b0c635d 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx
@@ -19,7 +19,6 @@ import {
import { useAppForm } from '@/components/ui/tanstack-form';
import { Textarea } from '@/components/ui/textarea';
import {
- type Credential3rdInsertDto,
Credential3rdInsertSchema,
INSERT_CREDENTIAL_3RD,
} from '@/domains/recorder/schema/credential3rd';
@@ -27,6 +26,7 @@ import { useInject } from '@/infra/di/inject';
import {
Credential3rdTypeEnum,
type InsertCredential3rdMutation,
+ type InsertCredential3rdMutationVariables,
} from '@/infra/graphql/gql/graphql';
import { PlatformService } from '@/infra/platform/platform.service';
import type { RouteStateDataOption } from '@/infra/routes/traits';
@@ -47,7 +47,8 @@ function CredentialCreateRouteComponent() {
const platformService = useInject(PlatformService);
const [insertCredential3rd, { loading }] = useMutation<
- InsertCredential3rdMutation['credential3rdCreateOne']
+ InsertCredential3rdMutation['credential3rdCreateOne'],
+ InsertCredential3rdMutationVariables
>(INSERT_CREDENTIAL_3RD, {
onCompleted(data) {
toast.success('Credential created');
@@ -69,16 +70,20 @@ function CredentialCreateRouteComponent() {
username: '',
password: '',
userAgent: '',
- } as Credential3rdInsertDto,
+ },
validators: {
onBlur: Credential3rdInsertSchema,
onSubmit: Credential3rdInsertSchema,
},
onSubmit: async (form) => {
+ const value = {
+ ...form.value,
+ userAgent: form.value.userAgent || platformService.userAgent,
+ };
if (form.value.credentialType === Credential3rdTypeEnum.Mikan) {
await insertCredential3rd({
variables: {
- data: form.value,
+ data: value,
},
});
}
@@ -142,9 +147,7 @@ function CredentialCreateRouteComponent() {
{field.state.meta.errors && (
-
- {field.state.meta.errors[0]?.toString()}
-
+
)}
)}
@@ -209,7 +212,7 @@ function CredentialCreateRouteComponent() {
the default value"
/>
- Current user agent: {platformService.userAgent}
+ Current default user agent: {platformService.userAgent}
{field.state.meta.errors && (
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
index d03e109..fe0d832 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx
@@ -1,9 +1,214 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { DetailEmptyView } from '@/components/ui/detail-empty-view';
+import { Label } from '@/components/ui/label';
+import { QueryErrorView } from '@/components/ui/query-error-view';
+import { Separator } from '@/components/ui/separator';
+import { GET_CREDENTIAL_3RD_DETAIL } from '@/domains/recorder/schema/credential3rd';
+import type { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
+import type { RouteStateDataOption } from '@/infra/routes/traits';
+import { useQuery } from '@apollo/client';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { format } from 'date-fns/format';
+import { ArrowLeft, Edit, Eye, EyeOff } from 'lucide-react';
+import { useState } from 'react';
export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
- component: RouteComponent,
-})
+ component: Credential3rdDetailRouteComponent,
+ staticData: {
+ breadcrumb: { label: 'Detail' },
+ } satisfies RouteStateDataOption,
+});
-function RouteComponent() {
- return Hello "/_app/credential3rd/detail/$id"!
+function Credential3rdDetailRouteComponent() {
+ const { id } = Route.useParams();
+ const navigate = useNavigate();
+ const [showPassword, setShowPassword] = useState(false);
+
+ const { loading, error, data } = useQuery(
+ GET_CREDENTIAL_3RD_DETAIL,
+ {
+ variables: {
+ id: Number.parseInt(id),
+ },
+ fetchPolicy: 'cache-and-network',
+ }
+ );
+
+ const handleBack = () => {
+ navigate({
+ to: '/credential3rd/manage',
+ });
+ };
+
+ const handleEnterEditMode = () => {
+ navigate({
+ to: '/credential3rd/edit/$id',
+ params: {
+ id,
+ },
+ });
+ };
+
+ const togglePasswordVisibility = () => {
+ setShowPassword((prev) => !prev);
+ };
+
+ const credential = data?.credential3rd?.nodes?.[0];
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ if (!credential) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
Credential detail
+
+ View and manage credential #{credential.id}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Credential information
+
+ View credential detail
+
+
+
+ {credential.credentialType}
+
+
+
+
+
+
+
+
+
+ {credential.id}
+
+
+
+
+
+
+
+ {credential.credentialType}
+
+
+
+
+
+
+
+
+ {credential.username || 'Not set'}
+
+
+
+
+
+
+
+
+ {showPassword ? credential.password || '-' : '••••••••'}
+
+ {credential.password && (
+
+ )}
+
+
+
+
+
+
+
+
+ {credential.userAgent || '-'}
+
+
+
+
+
+
+
+
+
+
+
+ {format(
+ new Date(credential.createdAt),
+ 'yyyy-MM-dd HH:mm:ss'
+ )}
+
+
+
+
+
+
+
+
+ {format(
+ new Date(credential.updatedAt),
+ 'yyyy-MM-dd HH:mm:ss'
+ )}
+
+
+
+
+
+
+
+
+ );
}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx
new file mode 100644
index 0000000..4f89961
--- /dev/null
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/edit.$id.tsx
@@ -0,0 +1,292 @@
+import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { DetailEmptyView } from '@/components/ui/detail-empty-view';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { QueryErrorView } from '@/components/ui/query-error-view';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { useAppForm } from '@/components/ui/tanstack-form';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ type Credential3rdDetailDto,
+ Credential3rdUpdateSchema,
+ GET_CREDENTIAL_3RD_DETAIL,
+ UPDATE_CREDENTIAL_3RD,
+} from '@/domains/recorder/schema/credential3rd';
+import type {
+ Credential3rdTypeEnum,
+ GetCredential3rdDetailQuery,
+ UpdateCredential3rdMutation,
+ UpdateCredential3rdMutationVariables,
+} from '@/infra/graphql/gql/graphql';
+import type { RouteStateDataOption } from '@/infra/routes/traits';
+import { useMutation, useQuery } from '@apollo/client';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
+import { useCallback, useState } from 'react';
+import { toast } from 'sonner';
+
+export const Route = createFileRoute('/_app/credential3rd/edit/$id')({
+ component: Credential3rdEditRouteComponent,
+ staticData: {
+ breadcrumb: { label: 'Edit' },
+ } satisfies RouteStateDataOption,
+});
+
+function FormView({
+ credential,
+ onCompleted,
+}: {
+ credential: Credential3rdDetailDto;
+ onCompleted: VoidFunction;
+}) {
+ const navigate = useNavigate();
+ const [showPassword, setShowPassword] = useState(false);
+ const togglePasswordVisibility = () => {
+ setShowPassword((prev) => !prev);
+ };
+
+ const handleBack = () => {
+ navigate({
+ to: '/credential3rd/manage',
+ });
+ };
+
+ const [updateCredential, { loading: updating }] = useMutation<
+ UpdateCredential3rdMutation['credential3rdUpdate'],
+ UpdateCredential3rdMutationVariables
+ >(UPDATE_CREDENTIAL_3RD, {
+ onCompleted,
+ onError: (error) => {
+ toast('Update credential failed', {
+ description: error.message,
+ });
+ },
+ });
+
+ const form = useAppForm({
+ defaultValues: {
+ credentialType: credential.credentialType,
+ username: credential.username,
+ password: credential.password,
+ userAgent: credential.userAgent,
+ },
+ validators: {
+ onBlur: Credential3rdUpdateSchema,
+ onSubmit: Credential3rdUpdateSchema,
+ },
+ onSubmit: (form) => {
+ const value = form.value;
+ updateCredential({
+ variables: {
+ data: value,
+ filters: {
+ id: {
+ eq: credential.id,
+ },
+ },
+ },
+ });
+ },
+ });
+
+ return (
+
+
+
+
+
+
Credential detail
+
+ View and manage credential #{credential.id}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Credential information
+
+ Edit credential information
+
+
+
+ {credential.credentialType}
+
+
+
+
+
+ {(field) => (
+
+
+
+
+ )}
+
+
+
+ {(field) => (
+
+
+ field.handleChange(e.target.value)}
+ placeholder="Please enter username"
+ />
+
+ )}
+
+
+
+ {(field) => (
+
+
+
+ field.handleChange(e.target.value)}
+ placeholder="Please enter password"
+ className="pr-10"
+ />
+
+
+
+ )}
+
+
+ {(field) => (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+function Credential3rdEditRouteComponent() {
+ const { id } = Route.useParams();
+
+ const { loading, error, data, refetch } =
+ useQuery(GET_CREDENTIAL_3RD_DETAIL, {
+ variables: {
+ id: Number.parseInt(id),
+ },
+ fetchPolicy: 'cache-and-network',
+ });
+
+ const credential = data?.credential3rd?.nodes?.[0];
+
+ const onCompleted = useCallback(async () => {
+ const refetchResult = await refetch();
+ if (refetchResult.errors) {
+ toast('Update credential failed', {
+ description: refetchResult.errors[0].message,
+ });
+ } else {
+ toast('Update credential successfully');
+ }
+ }, [refetch]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ if (!credential) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
index 712a661..a8d0601 100644
--- a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
+++ b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx
@@ -36,7 +36,6 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
-import { zhCN } from 'date-fns/locale';
import { Eye, EyeOff, Plus } from 'lucide-react';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';
@@ -51,7 +50,10 @@ export const Route = createFileRoute('/_app/credential3rd/manage')({
function CredentialManageRouteComponent() {
const navigate = useNavigate();
- const [columnVisibility, setColumnVisibility] = useState({});
+ const [columnVisibility, setColumnVisibility] = useState({
+ createdAt: false,
+ updatedAt: false,
+ });
const [sorting, setSorting] = useState([]);
const [pagination, setPagination] = useState({
pageIndex: 0,
@@ -85,7 +87,9 @@ function CredentialManageRouteComponent() {
onCompleted: async () => {
const refetchResult = await refetch();
if (refetchResult.errors) {
- toast.error(refetchResult.errors[0].message);
+ toast.error('Failed to delete credential', {
+ description: refetchResult.errors[0].message,
+ });
return;
}
toast.success('Credential deleted');
@@ -124,7 +128,6 @@ function CredentialManageRouteComponent() {
{
header: 'ID',
accessorKey: 'id',
- enableHiding: false,
cell: ({ row }) => {
return {row.original.id}
;
},
@@ -160,14 +163,10 @@ function CredentialManageRouteComponent() {
const password = row.original.password;
const isVisible = showPasswords[row.original.id];
- if (!password) {
- return -
;
- }
-
return (
- {isVisible ? password : '••••••••'}
+ {isVisible ? password || '-' : '••••••••'}
);
},
@@ -231,12 +228,19 @@ function CredentialManageRouteComponent() {
getId={(row) => row.original.id}
showEdit
showDelete
- onEdit={() => {
+ showDetail
+ onDetail={() => {
navigate({
to: '/credential3rd/detail/$id',
params: { id: `${row.original.id}` },
});
}}
+ onEdit={() => {
+ navigate({
+ to: '/credential3rd/edit/$id',
+ params: { id: `${row.original.id}` },
+ });
+ }}
onDelete={handleDeleteRecord(row)}
/>
),
@@ -260,6 +264,13 @@ function CredentialManageRouteComponent() {
sorting,
columnVisibility,
},
+ enableColumnPinning: true,
+ initialState: {
+ columnPinning: {
+ left: [],
+ right: ['actions'],
+ },
+ },
});
if (error) {
diff --git a/apps/webui/src/presentation/routes/auth/oidc/callback.tsx b/apps/webui/src/presentation/routes/auth/oidc/callback.tsx
index 54c70cd..f8fa67d 100644
--- a/apps/webui/src/presentation/routes/auth/oidc/callback.tsx
+++ b/apps/webui/src/presentation/routes/auth/oidc/callback.tsx
@@ -4,7 +4,7 @@ import { ProLink } from '@/components/ui/pro-link';
import { Spinner } from '@/components/ui/spinner';
import { AUTH_METHOD } from '@/infra/auth/defs';
import { createFileRoute, redirect } from '@tanstack/react-router';
-import { useAtom } from 'jotai/react';
+import { useAtomValue } from 'jotai/react';
import { atomWithObservable } from 'jotai/utils';
import { EventTypes } from 'oidc-client-rx';
import { useMemo } from 'react';
@@ -23,7 +23,7 @@ export const Route = createFileRoute('/auth/oidc/callback')({
function OidcCallbackRouteComponent() {
const { authService } = useAuth();
- const isLoading = useAtom(
+ const isLoading = useAtomValue(
useMemo(
() =>
atomWithObservable(() =>
@@ -33,7 +33,7 @@ function OidcCallbackRouteComponent() {
)
);
- const checkAuthResultError = useAtom(
+ const checkAuthResultError = useAtomValue(
useMemo(
() =>
atomWithObservable(() =>
diff --git a/biome.json b/biome.json
index 381be21..71ec6aa 100644
--- a/biome.json
+++ b/biome.json
@@ -4,6 +4,7 @@
"javascript": {
"globals": ["Liveblocks"]
},
+
"linter": {
"rules": {
"nursery": {
@@ -26,6 +27,12 @@
"noSvgWithoutTitle": "off"
},
"complexity": {
+ "noExcessiveCognitiveComplexity": {
+ "level": "warn",
+ "options": {
+ "maxAllowedComplexity": 20
+ }
+ },
"noBannedTypes": "off"
},
"correctness": {