zudoku 0.1.1-dev.30 → 0.1.1-dev.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/app/App.js +7 -2
  2. package/dist/app/App.js.map +1 -1
  3. package/dist/config/config.d.ts +5 -0
  4. package/dist/lib/components/Header.js +1 -1
  5. package/dist/lib/components/Header.js.map +1 -1
  6. package/dist/lib/components/Layout.js +1 -1
  7. package/dist/lib/components/Layout.js.map +1 -1
  8. package/dist/lib/components/SyntaxHighlight.d.ts +1 -0
  9. package/dist/lib/components/SyntaxHighlight.js +1 -1
  10. package/dist/lib/components/SyntaxHighlight.js.map +1 -1
  11. package/dist/lib/components/context/DevPortalProvider.js +1 -1
  12. package/dist/lib/components/context/DevPortalProvider.js.map +1 -1
  13. package/dist/lib/components/navigation/SideNavigation.js +1 -1
  14. package/dist/lib/components/navigation/SideNavigation.js.map +1 -1
  15. package/dist/lib/components/navigation/SideNavigationCategory.js +1 -1
  16. package/dist/lib/components/navigation/SideNavigationCategory.js.map +1 -1
  17. package/dist/lib/components/navigation/SideNavigationItem.d.ts +1 -0
  18. package/dist/lib/components/navigation/SideNavigationItem.js +8 -1
  19. package/dist/lib/components/navigation/SideNavigationItem.js.map +1 -1
  20. package/dist/lib/components/navigation/SideNavigationWrapper.d.ts +3 -0
  21. package/dist/lib/components/navigation/SideNavigationWrapper.js +2 -3
  22. package/dist/lib/components/navigation/SideNavigationWrapper.js.map +1 -1
  23. package/dist/lib/core/DevPortalContext.d.ts +1 -0
  24. package/dist/lib/core/DevPortalContext.js +2 -2
  25. package/dist/lib/core/DevPortalContext.js.map +1 -1
  26. package/dist/lib/core/plugins.d.ts +1 -1
  27. package/dist/lib/core/plugins.js +1 -1
  28. package/dist/lib/core/plugins.js.map +1 -1
  29. package/dist/lib/plugins/api-key/CreateApiKeys.d.ts +5 -0
  30. package/dist/lib/plugins/api-key/CreateApiKeys.js +37 -0
  31. package/dist/lib/plugins/api-key/CreateApiKeys.js.map +1 -0
  32. package/dist/lib/plugins/api-key/SettingsApiKeys.d.ts +3 -2
  33. package/dist/lib/plugins/api-key/SettingsApiKeys.js +30 -4
  34. package/dist/lib/plugins/api-key/SettingsApiKeys.js.map +1 -1
  35. package/dist/lib/plugins/api-key/index.d.ts +18 -21
  36. package/dist/lib/plugins/api-key/index.js +49 -24
  37. package/dist/lib/plugins/api-key/index.js.map +1 -1
  38. package/dist/lib/plugins/markdown/MdxPage.js +1 -1
  39. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  40. package/dist/lib/plugins/openapi/MakeRequest.js +22 -12
  41. package/dist/lib/plugins/openapi/MakeRequest.js.map +1 -1
  42. package/dist/lib/plugins/openapi/OperationListItem.js +24 -1
  43. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  44. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js +1 -1
  45. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js.map +1 -1
  46. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js +2 -2
  47. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js.map +1 -1
  48. package/dist/lib/plugins/openapi/Sidecar.js +13 -13
  49. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  50. package/dist/lib/plugins/openapi/graphql/gql.d.ts +2 -2
  51. package/dist/lib/plugins/openapi/graphql/gql.js +1 -1
  52. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  53. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +1 -0
  54. package/dist/lib/plugins/openapi/graphql/graphql.js +4 -0
  55. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  56. package/dist/lib/plugins/openapi/index.js +2 -0
  57. package/dist/lib/plugins/openapi/index.js.map +1 -1
  58. package/dist/lib/plugins/openapi/playground/Headers.js +5 -16
  59. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  60. package/dist/lib/plugins/openapi/playground/InlineInput.js +1 -1
  61. package/dist/lib/plugins/openapi/playground/{UrlParts.d.ts → PathParams.d.ts} +2 -2
  62. package/dist/lib/plugins/openapi/playground/{UrlParts.js → PathParams.js} +4 -4
  63. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -0
  64. package/dist/lib/plugins/openapi/playground/Playground.d.ts +7 -4
  65. package/dist/lib/plugins/openapi/playground/Playground.js +48 -46
  66. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  67. package/dist/lib/plugins/openapi/playground/QueryParams.d.ts +4 -6
  68. package/dist/lib/plugins/openapi/playground/QueryParams.js +27 -22
  69. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  70. package/dist/lib/plugins/openapi/playground/UrlDisplay.d.ts +4 -0
  71. package/dist/lib/plugins/openapi/playground/UrlDisplay.js +22 -0
  72. package/dist/lib/plugins/openapi/playground/UrlDisplay.js.map +1 -0
  73. package/dist/lib/plugins/openapi/playground/createUrl.d.ts +2 -0
  74. package/dist/lib/plugins/openapi/playground/createUrl.js +15 -0
  75. package/dist/lib/plugins/openapi/playground/createUrl.js.map +1 -0
  76. package/dist/lib/plugins/openapi/util/generateSchemaExample.d.ts +1 -1
  77. package/dist/lib/plugins/openapi/util/generateSchemaExample.js +19 -9
  78. package/dist/lib/plugins/openapi/util/generateSchemaExample.js.map +1 -1
  79. package/dist/lib/util/MdxComponents.js +1 -1
  80. package/dist/lib/util/MdxComponents.js.map +1 -1
  81. package/dist/vite/plugin-api-keys.d.ts +4 -0
  82. package/dist/vite/plugin-api-keys.js +33 -0
  83. package/dist/vite/plugin-api-keys.js.map +1 -0
  84. package/dist/vite/plugin-component.js +8 -20
  85. package/dist/vite/plugin-component.js.map +1 -1
  86. package/dist/vite/plugin-mdx.js +1 -1
  87. package/dist/vite/plugin-mdx.js.map +1 -1
  88. package/dist/vite/plugin.js +2 -0
  89. package/dist/vite/plugin.js.map +1 -1
  90. package/lib/{urql-B7mLfVog.js → urql-DMlBWUKL.js} +301 -321
  91. package/lib/{DevPortal-COMmOqxP.js → util-CJko6Ria.js} +3661 -4915
  92. package/lib/zudoku.components.js +1313 -4
  93. package/lib/zudoku.openapi-worker.js +1 -1
  94. package/lib/zudoku.plugins.js +16287 -15966
  95. package/package.json +1 -1
  96. package/src/app/App.tsx +7 -2
  97. package/src/lib/components/Header.tsx +3 -3
  98. package/src/lib/components/Layout.tsx +6 -1
  99. package/src/lib/components/SyntaxHighlight.tsx +6 -3
  100. package/src/lib/components/context/DevPortalProvider.ts +1 -1
  101. package/src/lib/components/navigation/SideNavigation.tsx +4 -1
  102. package/src/lib/components/navigation/SideNavigationCategory.tsx +3 -1
  103. package/src/lib/components/navigation/SideNavigationItem.tsx +8 -1
  104. package/src/lib/components/navigation/SideNavigationWrapper.tsx +14 -11
  105. package/src/lib/core/DevPortalContext.ts +3 -2
  106. package/src/lib/core/plugins.ts +1 -1
  107. package/src/lib/plugins/api-key/CreateApiKeys.tsx +84 -0
  108. package/src/lib/plugins/api-key/SettingsApiKeys.tsx +105 -11
  109. package/src/lib/plugins/api-key/index.tsx +71 -66
  110. package/src/lib/plugins/markdown/MdxPage.tsx +2 -2
  111. package/src/lib/plugins/openapi/MakeRequest.tsx +25 -23
  112. package/src/lib/plugins/openapi/OperationListItem.tsx +137 -1
  113. package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +1 -1
  114. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +1 -2
  115. package/src/lib/plugins/openapi/Sidecar.tsx +20 -20
  116. package/src/lib/plugins/openapi/graphql/gql.ts +3 -3
  117. package/src/lib/plugins/openapi/graphql/graphql.ts +5 -0
  118. package/src/lib/plugins/openapi/index.tsx +2 -0
  119. package/src/lib/plugins/openapi/playground/Headers.tsx +14 -20
  120. package/src/lib/plugins/openapi/playground/InlineInput.tsx +1 -1
  121. package/src/lib/plugins/openapi/playground/{UrlParts.tsx → PathParams.tsx} +22 -26
  122. package/src/lib/plugins/openapi/playground/Playground.tsx +205 -155
  123. package/src/lib/plugins/openapi/playground/QueryParams.tsx +84 -57
  124. package/src/lib/plugins/openapi/playground/UrlDisplay.tsx +32 -0
  125. package/src/lib/plugins/openapi/playground/createUrl.ts +22 -0
  126. package/src/lib/plugins/openapi/util/generateSchemaExample.ts +24 -9
  127. package/src/lib/util/MdxComponents.tsx +1 -0
  128. package/dist/lib/plugins/openapi/playground/UrlParts.js.map +0 -1
  129. package/src/lib/plugins/openapi/queries.graphql +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.1.1-dev.30",
3
+ "version": "0.1.1-dev.32",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
package/src/app/App.tsx CHANGED
@@ -7,9 +7,10 @@ import config from "virtual:zudoku-config";
7
7
  import { configuredApiPlugins } from "virtual:zudoku-api-plugins";
8
8
  import { configuredAuthProvider } from "virtual:zudoku-auth";
9
9
  import { configuredDocsPlugins } from "virtual:zudoku-docs-plugins";
10
+ import { configuredApiKeysPlugin } from "virtual:zudoku-api-keys-plugin";
10
11
 
11
12
  // Base React Component
12
- import { DevPortal } from "virtual:zudoku-component";
13
+ import { DevPortal } from "zudoku/components";
13
14
 
14
15
  // IMPORTANT: This component must not contain tailwind classes
15
16
  // This directory is not processed by the tailwind plugin
@@ -27,7 +28,11 @@ export default function App() {
27
28
  }}
28
29
  navigation={config.navigation ?? []}
29
30
  authentication={configuredAuthProvider}
30
- plugins={[...configuredDocsPlugins, ...configuredApiPlugins]}
31
+ plugins={[
32
+ ...configuredDocsPlugins,
33
+ ...configuredApiPlugins,
34
+ configuredApiKeysPlugin,
35
+ ]}
31
36
  />
32
37
  );
33
38
  }
@@ -25,7 +25,7 @@ export const Header = memo(() => {
25
25
  </span>
26
26
  </div>
27
27
  <div className="grid grid-cols-[--sidecar-grid-cols] items-center gap-8">
28
- <div className="w-full max-w-prose mx-auto">
28
+ <div className="w-full max-w-prose">
29
29
  <button className="flex items-center border border-input hover:bg-accent hover:text-accent-foreground p-4 relative h-8 justify-start rounded-lg bg-background text-sm text-muted-foreground shadow-none w-40 sm:w-72">
30
30
  <div className="flex items-center gap-2 flex-grow">
31
31
  <SearchIcon size={14} />
@@ -37,9 +37,9 @@ export const Header = memo(() => {
37
37
  </button>
38
38
  </div>
39
39
  <div className="items-center justify-self-end text-sm hidden lg:flex">
40
- {!isLoggedIn ? (
40
+ {isLoggedIn ? (
41
41
  <button
42
- className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded"
42
+ className="cursor-pointer hover:bg-secondary p-1 px-2 mx-2 rounded text-nowrap"
43
43
  onClick={logout}
44
44
  >
45
45
  Logout {email ? `(${email})` : null}
@@ -47,7 +47,12 @@ export const Layout = ({ children }: { children?: ReactNode }) => {
47
47
  >
48
48
  <SideNavigation />
49
49
  </Suspense>
50
- <main className="dark:border-white/10 lg:overflow-visible translate-x-0 lg:w-[calc(100%-var(--side-nav-width))] lg:translate-x-[--side-nav-width] pl-12">
50
+ <main
51
+ className="dark:border-white/10 translate-x-0
52
+ lg:overflow-visible
53
+ lg:peer-data-[navigation=true]:w-[calc(100%-var(--side-nav-width))]
54
+ lg:peer-data-[navigation=true]:translate-x-[--side-nav-width] peer-data-[navigation=true]:pl-12"
55
+ >
51
56
  {children ?? <Outlet />}
52
57
  </main>
53
58
  </div>
@@ -32,6 +32,7 @@ type SyntaxHighlightProps = {
32
32
  noBackground?: boolean;
33
33
  wrapLines?: boolean;
34
34
  copyable?: boolean;
35
+ showLanguageIndicator?: boolean;
35
36
  } & Omit<HighlightProps, "children">;
36
37
 
37
38
  export const SyntaxHighlight = ({
@@ -81,9 +82,11 @@ export const SyntaxHighlight = ({
81
82
  )}
82
83
  </button>
83
84
  )}
84
- <span className="absolute top-1.5 right-3 text-[11px] font-mono text-muted-foreground transition group-hover:opacity-0">
85
- {props.language}
86
- </span>
85
+ {props.showLanguageIndicator && (
86
+ <span className="absolute top-1.5 right-3 text-[11px] font-mono text-muted-foreground transition group-hover:opacity-0">
87
+ {props.language}
88
+ </span>
89
+ )}
87
90
  {tokens.map((line, i) => (
88
91
  <div key={i} {...getLineProps({ line })}>
89
92
  {line.map((token, key) => (
@@ -23,7 +23,7 @@ export const useApiIdentities = () => {
23
23
  const { getApiIdentities } = useDevPortal();
24
24
  return useSuspenseQuery({
25
25
  queryFn: getApiIdentities,
26
- queryKey: ["api-keys"],
26
+ queryKey: ["api-identities"],
27
27
  });
28
28
  };
29
29
 
@@ -9,7 +9,10 @@ export const SideNavigation = () => {
9
9
  const navigation = useNavigation();
10
10
 
11
11
  return (
12
- <SideNavigationWrapper ref={navRef}>
12
+ <SideNavigationWrapper
13
+ ref={navRef}
14
+ data={navigation.data.items.length > 0 ? "true" : "false"}
15
+ >
13
16
  {navigation.data.items.map((category) => (
14
17
  <SideNavigationCategory key={category.label} category={category} />
15
18
  ))}
@@ -36,7 +36,7 @@ export const SideNavigationCategory = ({
36
36
  open={isOpen}
37
37
  onOpenChange={() => setIsOpen((prev) => !prev)}
38
38
  >
39
- {category.label.length > 0 && (
39
+ {category.label.length > 0 ? (
40
40
  <Collapsible.Trigger asChild={isCollapsible} disabled={!isCollapsible}>
41
41
  <h5
42
42
  className={cn(
@@ -53,6 +53,8 @@ export const SideNavigationCategory = ({
53
53
  )}
54
54
  </h5>
55
55
  </Collapsible.Trigger>
56
+ ) : (
57
+ "Endpoints"
56
58
  )}
57
59
  <Collapsible.Content className="CollapsibleContent -mx-[--padding-nav-item]">
58
60
  {/* margin on Collapsible.Content will lead jumpiness when animating because it's not added to the calculated height */}
@@ -23,6 +23,10 @@ export const linkItem = cva(
23
23
  true: "text-primary font-medium",
24
24
  false: "text-foreground/80",
25
25
  },
26
+ isMuted: {
27
+ true: "opacity-60",
28
+ false: "",
29
+ },
26
30
  },
27
31
  },
28
32
  );
@@ -54,7 +58,10 @@ export const SideNavigationItem = ({
54
58
  if (isLinkItem(item)) {
55
59
  const classes = cn(
56
60
  "flex items-center gap-2",
57
- linkItem({ isActive: item.href === location.pathname }),
61
+ linkItem({
62
+ isActive: item.href === location.pathname,
63
+ isMuted: true,
64
+ }),
58
65
  );
59
66
  return item.href.startsWith("http") ? (
60
67
  <a
@@ -1,15 +1,18 @@
1
1
  import { forwardRef, type PropsWithChildren } from "react";
2
+ import { cn } from "../../util/cn.js";
2
3
 
3
4
  export const SideNavigationWrapper = forwardRef<
4
5
  HTMLDivElement,
5
- PropsWithChildren
6
- >(({ children }, ref) => {
7
- return (
8
- <nav
9
- className="hidden lg:flex flex-col fixed text-sm overflow-y-auto shrink-0 p-[--padding-nav-item] -mx-[--padding-nav-item] pb-20 pt-[--padding-content-top] w-[--side-nav-width] h-[calc(100%-var(--header-height))] scroll-pt-2 gap-2"
10
- ref={ref}
11
- >
12
- {children}
13
- </nav>
14
- );
15
- });
6
+ PropsWithChildren<{ data?: string; className?: string }>
7
+ >(({ children, className, data }, ref) => (
8
+ <nav
9
+ data-navigation={data}
10
+ className={cn(
11
+ "peer hidden lg:flex flex-col fixed text-sm overflow-y-auto shrink-0 p-[--padding-nav-item] -mx-[--padding-nav-item] pb-20 pt-[--padding-content-top] w-[--side-nav-width] h-[calc(100%-var(--header-height))] scroll-pt-2 gap-2",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ >
16
+ {children}
17
+ </nav>
18
+ ));
@@ -7,7 +7,7 @@ import {
7
7
  type DevPortalProps,
8
8
  } from "../components/DevPortal.js";
9
9
  import {
10
- isApiKeyPlugin,
10
+ isApiIdentityPlugin,
11
11
  isNavigationPlugin,
12
12
  needsInitialization,
13
13
  type DevPortalPlugin,
@@ -26,6 +26,7 @@ export interface ApiIdentity {
26
26
  type BaseNavigationCategoryItem = {
27
27
  label: ReactNode;
28
28
  title?: string;
29
+ muted?: boolean;
29
30
  icon?: ReactNode | IconName;
30
31
  };
31
32
 
@@ -117,7 +118,7 @@ export class DevPortalContext {
117
118
  getApiIdentities = async () => {
118
119
  const keys = await Promise.all(
119
120
  this.plugins
120
- .filter(isApiKeyPlugin)
121
+ .filter(isApiIdentityPlugin)
121
122
  .map((plugin) => plugin.getIdentities(this)),
122
123
  );
123
124
 
@@ -37,7 +37,7 @@ export const needsInitialization = (
37
37
  ): obj is InitializationPlugin =>
38
38
  "initialize" in obj && typeof obj.initialize === "function";
39
39
 
40
- export const isApiKeyPlugin = (
40
+ export const isApiIdentityPlugin = (
41
41
  obj: DevPortalPlugin,
42
42
  ): obj is ApiIdentityPlugin =>
43
43
  "getIdentities" in obj && typeof obj.getIdentities === "function";
@@ -0,0 +1,84 @@
1
+ import { useDevPortal } from "../../components/context/DevPortalProvider.js";
2
+ import { type ApiKeyPluginOptions, ApiKeyService } from "./index.js";
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { Button } from "../../ui/Button.js";
5
+ import { Input } from "../../components/Input.js";
6
+ import { cn } from "../../util/cn.js";
7
+ import { Link, useNavigate } from "react-router-dom";
8
+ import { useForm } from "react-hook-form";
9
+
10
+ type CreateApiKeys = { description: string; expiresAt?: string };
11
+
12
+ export const CreateApiKey = ({
13
+ options,
14
+ service,
15
+ }: {
16
+ options: ApiKeyPluginOptions;
17
+ service: ApiKeyService;
18
+ }) => {
19
+ const context = useDevPortal();
20
+ const navigate = useNavigate();
21
+ const form = useForm<CreateApiKeys>();
22
+ const createKeyMutation = useMutation({
23
+ mutationFn: ({ description, expiresAt }: CreateApiKeys) => {
24
+ if (!service.createKey) {
25
+ throw new Error("deleteKey not implemented");
26
+ }
27
+
28
+ return service.createKey({ description }, context);
29
+ },
30
+ onSuccess: () => navigate("/settings/api-keys/"),
31
+ });
32
+
33
+ if (!service.createKey) {
34
+ return null;
35
+ }
36
+
37
+ return (
38
+ <div className="max-w-screen-lg pt-[--padding-content-top] pb-[--padding-content-bottom]">
39
+ <div className="flex justify-between mb-4 border-b pb-1">
40
+ <h1 className="font-medium text-2xl">New API Keys</h1>
41
+ </div>
42
+ <form
43
+ onSubmit={form.handleSubmit((data) => {
44
+ if (data.expiresAt === "never") {
45
+ delete data.expiresAt;
46
+ }
47
+ createKeyMutation.mutate(data);
48
+ })}
49
+ >
50
+ <div className="flex gap-2 flex-col">
51
+ Note
52
+ <Input {...form.register("description")} />
53
+ Expiration
54
+ <select
55
+ className={cn(
56
+ "row-start-1 col-start-1 border border-input text-foreground px-2 py-1 pe-6",
57
+ "rounded-md appearance-none bg-zinc-50 hover:bg-white dark:bg-zinc-800 hover:dark:bg-zinc-800/75",
58
+ )}
59
+ {...form.register("expiresAt")}
60
+ >
61
+ <option value="never">Never</option>
62
+ {[7, 30, 60, 90].map((option) => (
63
+ <option value={addDaysToDate(option)} key={option}>
64
+ {option} Days
65
+ </option>
66
+ ))}
67
+ </select>
68
+ <div className="flex gap-2">
69
+ <Button>Generate Key</Button>
70
+ <Button variant="outline" asChild>
71
+ <Link to="/settings/api-keys/">Cancel</Link>
72
+ </Button>
73
+ </div>
74
+ </div>
75
+ </form>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ const addDaysToDate = (days: number): string => {
81
+ const date = new Date();
82
+ date.setDate(date.getDate() + days);
83
+ return date.toISOString().split("T")[0];
84
+ };
@@ -1,22 +1,116 @@
1
- import { useApiIdentities } from "../../components/context/DevPortalProvider.js";
2
- import { type ApiKeyPluginOptions } from "./index.js";
1
+ import { useDevPortal } from "../../components/context/DevPortalProvider.js";
2
+ import { type ApiKeyPluginOptions, ApiKeyService } from "./index.js";
3
+ import {
4
+ useMutation,
5
+ useQueryClient,
6
+ useSuspenseQuery,
7
+ } from "@tanstack/react-query";
8
+ import { Button } from "../../ui/Button.js";
9
+ import { Card } from "../../ui/Card.js";
10
+ import { EyeIcon, EyeOffIcon, RotateCwIcon, TrashIcon } from "lucide-react";
11
+ import { Link } from "react-router-dom";
12
+ import { useState } from "react";
3
13
 
4
14
  export const SettingsApiKeys = ({
5
15
  options,
16
+ service,
6
17
  }: {
7
18
  options: ApiKeyPluginOptions;
19
+ service: ApiKeyService;
8
20
  }) => {
9
- const keys = useApiIdentities();
21
+ const context = useDevPortal();
22
+ const queryClient = useQueryClient();
23
+ const { data } = useSuspenseQuery({
24
+ queryFn: () => service.getKeys(context),
25
+ queryKey: ["api-keys"],
26
+ });
27
+
28
+ const deleteKeyMutation = useMutation({
29
+ mutationFn: (id: string) => {
30
+ if (!service.deleteKey) {
31
+ throw new Error("deleteKey not implemented");
32
+ }
33
+
34
+ return service.deleteKey(id, context);
35
+ },
36
+ onSuccess: () => {
37
+ void queryClient.invalidateQueries({ queryKey: ["api-keys"] });
38
+ },
39
+ });
40
+
41
+ return (
42
+ <div className="max-w-screen-lg pt-[--padding-content-top] pb-[--padding-content-bottom]">
43
+ <div className="flex justify-between mb-4 border-b border-border pb-1">
44
+ <h1 className="font-medium text-2xl">API Keys</h1>
45
+ {service.createKey && (
46
+ <Button asChild>
47
+ <Link to="/settings/api-keys/new">Create API Key</Link>
48
+ </Button>
49
+ )}
50
+ </div>
51
+
52
+ <Card>
53
+ <ul className="grid grid-cols-[min-content_1fr_min-content] ">
54
+ {data?.map((key) => (
55
+ <li
56
+ className="border-b border-border p-5 grid grid-cols-subgrid col-span-full gap-2 items-center"
57
+ key={key.id}
58
+ >
59
+ <div className="flex flex-col gap-1 text-sm">
60
+ {key.description ?? key.id}
61
+ {key.expiresOn ? (
62
+ <div>Expires on {key.expiresOn}</div>
63
+ ) : (
64
+ key.createdOn && (
65
+ <div className="text-gray-500">
66
+ Created on {new Date(key.createdOn).toLocaleDateString()}
67
+ </div>
68
+ )
69
+ )}
70
+ </div>
71
+ <div className="items-center flex justify-center">
72
+ <RevealApiKey apiKey={key.key} />
73
+ </div>
74
+ <div className="flex gap-2">
75
+ {service.rollKey && (
76
+ <Button size="icon">
77
+ <RotateCwIcon size={16} />
78
+ </Button>
79
+ )}
80
+ {service.deleteKey && (
81
+ <Button
82
+ variant="destructive"
83
+ size="icon"
84
+ onClick={() => deleteKeyMutation.mutate(key.id)}
85
+ disabled={deleteKeyMutation.isPending}
86
+ >
87
+ <TrashIcon size={16} />
88
+ </Button>
89
+ )}
90
+ </div>
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ </Card>
95
+ </div>
96
+ );
97
+ };
98
+
99
+ const RevealApiKey = ({ apiKey }: { apiKey: string }) => {
100
+ const [revealed, setRevealed] = useState(false);
10
101
 
11
102
  return (
12
- <div className="prose dark:prose-invert">
13
- <h1>Who's who</h1>
14
- <button className="border">Create API Key</button>
15
- <ul>
16
- {keys.data.map((key) => (
17
- <li>{key.name}</li>
18
- ))}
19
- </ul>
103
+ <div className="flex gap-2 items-center text-sm">
104
+ <code className="border border-border rounded bg-gray-100 dark:bg-gray-950 p-1 font-mono">
105
+ {revealed ? apiKey : "".repeat(apiKey.length)}
106
+ </code>
107
+ <Button
108
+ variant="outline"
109
+ onClick={() => setRevealed((prev) => !prev)}
110
+ size="icon"
111
+ >
112
+ {revealed ? <EyeOffIcon size={16} /> : <EyeIcon size={16} />}
113
+ </Button>
20
114
  </div>
21
115
  );
22
116
  };
@@ -1,55 +1,34 @@
1
- import { DevPortalSystemPaths } from "../../components/DevPortal.js";
2
1
  import { DevPortalContext } from "../../core/DevPortalContext.js";
3
2
  import {
4
3
  type ApiIdentityPlugin,
5
4
  type DevPortalPlugin,
6
5
  } from "../../core/plugins.js";
7
6
  import { SettingsApiKeys } from "./SettingsApiKeys.js";
7
+ import { CreateApiKey } from "./CreateApiKeys.js";
8
8
 
9
- export type GetConsumerOptions =
10
- | {
11
- getConsumers: (context: DevPortalContext) => Promise<ConsumerData | void>;
12
- }
13
- | { consumerEndpoint: string };
9
+ export type ApiKeyResults = Promise<ApiKey[]>;
10
+ const DEFAULT_API_KEY_ENDPOINT =
11
+ "https://zudoku-rewiringamerica-main-ef9c9c0.d2.zuplo.dev";
14
12
 
15
- export type ApiKeyPluginOptions = {
16
- rollKey?: (
17
- id: string,
18
- consumerName: string,
13
+ export type ApiKeyService = {
14
+ getKeys: (context: DevPortalContext) => ApiKeyResults;
15
+ rollKey?: (id: string, context: DevPortalContext) => Promise<void>;
16
+ deleteKey?: (id: string, context: DevPortalContext) => Promise<void>;
17
+ updateKeyDescription?: (
18
+ apiKey: { jd: string; description: string },
19
19
  context: DevPortalContext,
20
20
  ) => Promise<void>;
21
- deleteKey?: (
22
- id: string,
23
- consumerName: string,
21
+ createKey?: (
22
+ apiKey: { description: string; expiresAt?: string },
24
23
  context: DevPortalContext,
25
24
  ) => Promise<void>;
26
- updateConsumerDescription?: (
27
- consumerName: string,
28
- description: string,
29
- context: DevPortalContext,
30
- ) => Promise<void>;
31
- createConsumer?: (
32
- description: string,
33
- context: DevPortalContext,
34
- ) => Promise<void>;
35
- deleteConsumer?: (
36
- consumerName: string,
37
- context: DevPortalContext,
38
- ) => Promise<void>;
39
- } & GetConsumerOptions;
25
+ };
40
26
 
41
- export interface ConsumerData {
42
- consumers: Consumer[];
43
- }
27
+ export type GetApiKeysOptions = ApiKeyService | { endpoint: string } | {};
44
28
 
45
- export interface Consumer {
46
- name: string;
47
- createdOn?: string;
48
- description?: string;
49
- apiKeys: ConsumerApiKey[];
50
- }
29
+ export type ApiKeyPluginOptions = {} & GetApiKeysOptions;
51
30
 
52
- export interface ConsumerApiKey {
31
+ export interface ApiKey {
53
32
  id: string;
54
33
  description?: string;
55
34
  createdOn?: string;
@@ -58,45 +37,67 @@ export interface ConsumerApiKey {
58
37
  key: string;
59
38
  }
60
39
 
61
- const defaultGetConsumers = async (context: DevPortalContext) => {
62
- const accessToken = await context.authentication?.getToken?.(context);
40
+ const createDefaultHandler = (endpoint: string): ApiKeyService => {
41
+ return {
42
+ deleteKey: async (id, context) => {
43
+ const accessToken = await context.authentication?.getToken?.(context);
63
44
 
64
- if (!accessToken) {
65
- throw new Error("No token found");
66
- }
45
+ if (!accessToken) {
46
+ throw new Error("Not authenticated");
47
+ }
67
48
 
68
- const consumers = await fetch(
69
- "https://zudoku-customer-main-b36fa2f.d2.zuplo.dev/v1/developer/api-keys",
70
- {
71
- headers: {
72
- Authorization: `Bearer ${accessToken}`,
73
- },
49
+ await fetch(endpoint + `/v1/developer/api-keys/${id}`, {
50
+ method: "DELETE",
51
+ headers: {
52
+ Authorization: `Bearer ${accessToken}`,
53
+ },
54
+ });
74
55
  },
75
- );
56
+ createKey: async (apiKey, context) => {
57
+ const accessToken = await context.authentication?.getToken?.(context);
76
58
 
77
- return { consumers: [await consumers.json()] };
78
- };
59
+ if (!accessToken) {
60
+ throw new Error("Not authenticated");
61
+ }
79
62
 
80
- export const apiKeyPlugin = (
81
- options: ApiKeyPluginOptions,
82
- ): DevPortalPlugin & ApiIdentityPlugin => {
83
- const getConsumers =
84
- "getConsumers" in options ? options.getConsumers : defaultGetConsumers;
63
+ await fetch(endpoint + `/v1/developer/api-keys`, {
64
+ method: "POST",
65
+ headers: {
66
+ Authorization: `Bearer ${accessToken}`,
67
+ "Content-Type": "application/json",
68
+ },
69
+ body: JSON.stringify(apiKey),
70
+ });
71
+ },
72
+ getKeys: async (context) => {
73
+ const accessToken = await context.authentication?.getToken?.(context);
85
74
 
86
- return {
87
- getNavigation: async (path: string) => {
88
- if (path !== DevPortalSystemPaths.Settings) {
75
+ if (!accessToken) {
89
76
  return [];
90
77
  }
91
78
 
92
- return [
93
- {
94
- path: "settings/api-keys",
95
- label: "API Keys",
96
- children: [],
79
+ const consumers = await fetch(endpoint + `/v1/developer/api-keys`, {
80
+ headers: {
81
+ Authorization: `Bearer ${accessToken}`,
97
82
  },
98
- ];
83
+ });
84
+
85
+ return await consumers.json();
99
86
  },
87
+ };
88
+ };
89
+
90
+ export const apiKeyPlugin = (
91
+ options: ApiKeyPluginOptions,
92
+ ): DevPortalPlugin & ApiIdentityPlugin => {
93
+ const service =
94
+ "getKeys" in options
95
+ ? options
96
+ : createDefaultHandler(
97
+ "endpoint" in options ? options.endpoint : DEFAULT_API_KEY_ENDPOINT,
98
+ );
99
+
100
+ return {
100
101
  getIdentities: async (context) => {
101
102
  return [];
102
103
  // throw new Error("Not implemented");
@@ -115,7 +116,11 @@ export const apiKeyPlugin = (
115
116
  return [
116
117
  {
117
118
  path: "/settings/api-keys",
118
- element: <SettingsApiKeys options={options} />,
119
+ element: <SettingsApiKeys options={options} service={service} />,
120
+ },
121
+ {
122
+ path: "/settings/api-keys/new",
123
+ element: <CreateApiKey options={options} service={service} />,
119
124
  },
120
125
  ];
121
126
  },
@@ -75,10 +75,10 @@ export const MdxPage = ({
75
75
  <div
76
76
  className={cn(
77
77
  ProseClasses,
78
- "mx-auto max-w-full xl:w-full xl:max-w-prose flex-1 flex-shrink pt-[--padding-content-top] pb-[--padding-content-bottom]",
78
+ "max-w-full xl:w-full xl:max-w-prose flex-1 flex-shrink pt-[--padding-content-top] pb-[--padding-content-bottom]",
79
79
  )}
80
80
  >
81
- <header className="-translate-y-1">
81
+ <header>
82
82
  {category && <CategoryHeading>{category}</CategoryHeading>}
83
83
  {title && (
84
84
  <Heading level={1} id={slugify(title, { lower: true })}>