diff --git a/apps/dashboard/src/app/(dashboard)/integrations/page.tsx b/apps/dashboard/src/app/(dashboard)/integrations/page.tsx index 086083b..8f2066a 100644 --- a/apps/dashboard/src/app/(dashboard)/integrations/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/integrations/page.tsx @@ -1,28 +1,17 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@ds-project/components'; +import { MainContent } from '@/components'; import { GithubProvider } from './providers/github/_components'; import { FigmaProvider } from './providers/figma/_components'; export default function Page() { return ( - - - -

Integrations

-
- -

Authorize and manage integrations

-
-
- + +
- - +
+
); } diff --git a/apps/dashboard/src/app/(dashboard)/integrations/providers/github/_components/provider.tsx b/apps/dashboard/src/app/(dashboard)/integrations/providers/github/_components/provider.tsx index 8b0d195..d378c99 100644 --- a/apps/dashboard/src/app/(dashboard)/integrations/providers/github/_components/provider.tsx +++ b/apps/dashboard/src/app/(dashboard)/integrations/providers/github/_components/provider.tsx @@ -56,30 +56,32 @@ export async function GithubProvider() { -
- - + {installation ? ( + + + - -
+ + + ) : null} - + */} ); } diff --git a/apps/dashboard/src/app/(dashboard)/layout.tsx b/apps/dashboard/src/app/(dashboard)/layout.tsx index da24a8f..e1f18c1 100644 --- a/apps/dashboard/src/app/(dashboard)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/layout.tsx @@ -21,8 +21,10 @@ export default async function RootLayout({ return ( - -
+
+ +
+
{children}
diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-release-tokens.action.ts b/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-release-tokens.action.ts new file mode 100644 index 0000000..b010a37 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-release-tokens.action.ts @@ -0,0 +1,130 @@ +'use server'; +import type { DesignTokens } from 'style-dictionary/types'; +import type { Octokit } from '@octokit/core'; +import { + getGithubInstallation, + getGithubIntegration, + getGithubRepository, +} from '@/lib/github'; +import { isAuthenticated } from '@/lib/supabase/server/utils/is-authenticated'; +import { config } from '@/config'; + +async function searchFileSha({ + installation, + owner, + path, + repo, + treeSha, + index = 0, +}: { + installation: Octokit; + owner: string; + repo: string; + treeSha: string; + path: string[]; + index?: number; +}) { + const { data: treeData } = await installation.request( + 'GET /repos/{owner}/{repo}/git/trees/{tree_sha}', + { + owner, + repo, + tree_sha: treeSha, + } + ); + + const treeOrFile = treeData.tree.find( + (treeItem) => treeItem.path === path[index] + ); + if (!treeOrFile?.sha) { + return null; + } + if (index === path.length - 1) { + return treeOrFile.sha; + } + + return searchFileSha({ + installation, + owner, + repo, + treeSha: treeOrFile.sha, + path, + index: index + 1, + }); +} + +export async function fetchReleaseTokens(releaseId: number) { + if (!(await isAuthenticated())) { + throw new Error('Not authenticated'); + } + + const repository = await getGithubRepository(); + const integration = await getGithubIntegration(); + const installation = await getGithubInstallation(integration); + + const { data: release } = await installation.request( + 'GET /repos/{owner}/{repo}/releases/{release_id}', + { + owner: repository.owner.login, + repo: repository.name, + release_id: releaseId, + } + ); + + const { data: tagName } = await installation.request( + 'GET /repos/{owner}/{repo}/git/ref/{ref}', + { + owner: repository.owner.login, + repo: repository.name, + ref: `tags/${release.tag_name}`, + } + ); + + // First, get the file sha from the commit + const { data: tagSha } = await installation.request( + 'GET /repos/{owner}/{repo}/git/tags/{tag_sha}', + { + owner: repository.owner.login, + repo: repository.name, + tag_sha: tagName.object.sha, + } + ); + + const { data: commitSha } = await installation.request( + 'GET /repos/{owner}/{repo}/git/commits/{commit_sha}', + { + owner: repository.owner.login, + repo: repository.name, + commit_sha: tagSha.object.sha, + } + ); + + const tokensPath = [...config.gitTokensPath.split('/'), 'tokens.json']; + const fileSha = await searchFileSha({ + installation, + owner: repository.owner.login, + treeSha: commitSha.tree.sha, + repo: repository.name, + path: tokensPath, + }); + + if (!fileSha) { + throw new Error('File not found'); + } + + const { data: file } = await installation.request( + 'GET /repos/{owner}/{repo}/git/blobs/{file_sha}', + { + owner: repository.owner.login, + repo: repository.name, + file_sha: fileSha, + } + ); + + const tokens = Buffer.from( + file.content, + file.encoding as BufferEncoding + ).toString(); + + return JSON.parse(tokens) as DesignTokens; +} diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-releases.action.ts b/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-releases.action.ts new file mode 100644 index 0000000..af4a26b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/tokens/_actions/fetch-releases.action.ts @@ -0,0 +1,27 @@ +'use server'; +import { + getGithubInstallation, + getGithubIntegration, + getGithubRepository, +} from '@/lib/github'; +import { isAuthenticated } from '@/lib/supabase/server/utils/is-authenticated'; + +export async function fetchReleases() { + if (!(await isAuthenticated())) { + throw new Error('Not authenticated'); + } + + const repository = await getGithubRepository(); + const integration = await getGithubIntegration(); + const installation = await getGithubInstallation(integration); + + const { data } = await installation.request( + 'GET /repos/{owner}/{repo}/releases', + { + owner: repository.owner.login, + repo: repository.name, + } + ); + + return data; +} diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_actions/index.ts b/apps/dashboard/src/app/(dashboard)/tokens/_actions/index.ts index 91d26bc..6323d8f 100644 --- a/apps/dashboard/src/app/(dashboard)/tokens/_actions/index.ts +++ b/apps/dashboard/src/app/(dashboard)/tokens/_actions/index.ts @@ -1,2 +1,2 @@ -export * from './update-tokens.action'; +export * from './fetch-releases.action'; export * from './tokens.action'; diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_actions/update-tokens.action.ts b/apps/dashboard/src/app/(dashboard)/tokens/_actions/update-tokens.action.ts deleted file mode 100644 index bcfaf18..0000000 --- a/apps/dashboard/src/app/(dashboard)/tokens/_actions/update-tokens.action.ts +++ /dev/null @@ -1,22 +0,0 @@ -'use server'; - -import type { DesignTokens } from 'style-dictionary/types'; -import { revalidatePath } from 'next/cache'; -import { pushFile } from '@/lib/github'; -import { isAuthenticated } from '@/lib/supabase/server/utils/is-authenticated'; -import { config } from '@/config'; - -export async function updateTokens(newTokens: DesignTokens) { - if (!(await isAuthenticated())) { - throw new Error('Not authenticated'); - } - - const base64Content = btoa(JSON.stringify(newTokens, null, 2)); - await pushFile({ - content: base64Content, - encoding: 'base64', - name: `${config.gitTokensPath}/tokens.json`, - }); - - revalidatePath('/tokens'); -} diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_components/index.ts b/apps/dashboard/src/app/(dashboard)/tokens/_components/index.ts index 4fd7a2c..5705139 100644 --- a/apps/dashboard/src/app/(dashboard)/tokens/_components/index.ts +++ b/apps/dashboard/src/app/(dashboard)/tokens/_components/index.ts @@ -1 +1 @@ -export * from './push-button'; +export * from './select-releases'; diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_components/push-button.tsx b/apps/dashboard/src/app/(dashboard)/tokens/_components/push-button.tsx deleted file mode 100644 index f0f5753..0000000 --- a/apps/dashboard/src/app/(dashboard)/tokens/_components/push-button.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; - -import { Button } from '@ds-project/components'; -import { useCallback } from 'react'; -import type { DesignTokens } from 'style-dictionary/types'; -import { updateTokens } from '../_actions/update-tokens.action'; - -export function PushButton({ tokens }: { tokens: DesignTokens }) { - const onClickHandler = useCallback(() => { - void updateTokens(tokens); - }, [tokens]); - - return ; -} diff --git a/apps/dashboard/src/app/(dashboard)/tokens/_components/select-releases.tsx b/apps/dashboard/src/app/(dashboard)/tokens/_components/select-releases.tsx new file mode 100644 index 0000000..2d158e4 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/tokens/_components/select-releases.tsx @@ -0,0 +1,72 @@ +'use client'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@ds-project/components'; +import { useCallback, useEffect, useState } from 'react'; +import type { DesignTokens } from 'style-dictionary/types'; +import { InstallRelease, JsonBlock } from '@/components'; +import type { fetchReleases } from '../_actions'; +import { fetchReleaseTokens } from '../_actions/fetch-release-tokens.action'; + +interface SelectReleasesProps { + releases: Awaited>; +} + +export function SelectReleases({ releases }: SelectReleasesProps) { + const [selectedRelease, setSelectedRelease] = + useState(); + const [tokens, setTokens] = useState(); + + const onReleaseChange = useCallback( + (selectedReleaseId: string) => { + const newSelectedRelease = releases.find( + (release) => String(release.id) === selectedReleaseId + ); + setSelectedRelease(newSelectedRelease); + }, + [releases] + ); + + useEffect(() => { + if (!selectedRelease?.id) return; + + fetchReleaseTokens(selectedRelease.id) + .then((_tokens) => { + setTokens(_tokens); + }) + .catch((error) => { + // eslint-disable-next-line no-console -- TODO: replace with monitoring + console.error('Error fetching release tokens', error); + }); + }, [selectedRelease?.id]); + + return ( + <> +
+ + {selectedRelease?.name ? ( + + ) : null} +
+ {tokens ? : null} + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/tokens/page.tsx b/apps/dashboard/src/app/(dashboard)/tokens/page.tsx index 7a88678..9bd25be 100644 --- a/apps/dashboard/src/app/(dashboard)/tokens/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/tokens/page.tsx @@ -1,54 +1,18 @@ -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Text, -} from '@ds-project/components'; -import { equals } from 'rambda'; -import { DiffBlock } from '@/components/diff-block/diff-block'; -import { requestTokens } from '../integrations/providers/github/_actions'; -import { fetchTokens } from './_actions'; -import { PushButton } from './_components'; +import { MainContent } from '@/components'; +import { fetchReleases } from './_actions'; +import { SelectReleases } from './_components'; export default async function Tokens() { - const tokens = await fetchTokens(); - const githubTokens = await requestTokens(); - - const areTokensEqual = equals(tokens, githubTokens); + const releases = await fetchReleases(); return ( -
- - - -

Tokens

-
- -

- This is a list of all tokens used in Figma converted to Style - Dictionary format. -

-
-
- - {Boolean(tokens) && Boolean(githubTokens) ? ( - - ) : ( - -

No tokens found

-
- )} -
- - {!areTokensEqual && tokens ? : null} - -
-
+ +
+ +
+
); } diff --git a/apps/dashboard/src/app/api/figma/design-tokens/route.ts b/apps/dashboard/src/app/api/figma/design-tokens/route.ts index 0e6b1f6..1e46c53 100644 --- a/apps/dashboard/src/app/api/figma/design-tokens/route.ts +++ b/apps/dashboard/src/app/api/figma/design-tokens/route.ts @@ -3,6 +3,8 @@ import { eq } from 'drizzle-orm'; import { database } from '@/lib/drizzle'; import { insertResourcesSchema, resourcesTable } from '@/lib/drizzle/schema'; import { isAuthenticated } from '@/lib/supabase/server/utils/is-authenticated'; +import { pushFile } from '@/lib/github'; +import { config } from '@/config'; export async function POST(request: NextRequest) { if (!(await isAuthenticated(request))) { @@ -14,12 +16,23 @@ export async function POST(request: NextRequest) { .pick({ designTokens: true, projectId: true }) .parse(await request.json()); + // Update database await database .update(resourcesTable) .set({ designTokens: validatedData.designTokens, }) .where(eq(resourcesTable.projectId, validatedData.projectId)); + + // Update Github - TODO: turn into "update integrations" actions + const base64Content = btoa( + JSON.stringify(validatedData.designTokens, null, 2) + ); + await pushFile({ + content: base64Content, + encoding: 'base64', + name: `${config.gitTokensPath}/tokens.json`, + }); } catch (error) { return new Response(JSON.stringify(error), { status: 400 }); } diff --git a/apps/dashboard/src/components/home-button/home-button.tsx b/apps/dashboard/src/components/home-button/home-button.tsx index 50c11dd..8ad3e64 100644 --- a/apps/dashboard/src/components/home-button/home-button.tsx +++ b/apps/dashboard/src/components/home-button/home-button.tsx @@ -6,7 +6,11 @@ import logo from './logo.svg'; export function HomeButton({ className }: { className?: string }) { return ( - + + ); +} diff --git a/apps/dashboard/src/components/json-block/json-block.tsx b/apps/dashboard/src/components/json-block/json-block.tsx index cb72463..02adb1a 100644 --- a/apps/dashboard/src/components/json-block/json-block.tsx +++ b/apps/dashboard/src/components/json-block/json-block.tsx @@ -14,5 +14,9 @@ const DynamicReactJson = dynamic(() => import('react-json-view'), { }); export function JsonBlock(props: ComponentProps) { - return ; + return ( +
+ +
+ ); } diff --git a/apps/dashboard/src/components/main-content/index.ts b/apps/dashboard/src/components/main-content/index.ts new file mode 100644 index 0000000..812910c --- /dev/null +++ b/apps/dashboard/src/components/main-content/index.ts @@ -0,0 +1 @@ +export * from './main-content'; diff --git a/apps/dashboard/src/components/main-content/main-content.tsx b/apps/dashboard/src/components/main-content/main-content.tsx new file mode 100644 index 0000000..a50547e --- /dev/null +++ b/apps/dashboard/src/components/main-content/main-content.tsx @@ -0,0 +1,32 @@ +import { Text } from '@ds-project/components'; +import type { ReactNode } from 'react'; + +interface MainContentProps { + title: string; + description: string; + children: ReactNode; +} + +export function MainContent({ + title, + description, + children, +}: MainContentProps) { + return ( +
+
+
+ +

{title}

+
+ +

{description}

+
+
+
+
+
{children}
+
+
+ ); +} diff --git a/apps/dashboard/src/components/navigation/navigation.tsx b/apps/dashboard/src/components/navigation/navigation.tsx index 8572670..c1699c6 100644 --- a/apps/dashboard/src/components/navigation/navigation.tsx +++ b/apps/dashboard/src/components/navigation/navigation.tsx @@ -22,7 +22,12 @@ interface NavigationProps { export function Navigation({ className, projects }: NavigationProps) { return ( -