diff --git a/.github/workflows/build-push-images.yml b/.github/workflows/build-push-images.yml index e93ebde76..fbd44cb26 100644 --- a/.github/workflows/build-push-images.yml +++ b/.github/workflows/build-push-images.yml @@ -79,9 +79,9 @@ jobs: env: NEXT_PUBLIC_API_URL: http://localhost:3000 NEXT_PUBLIC_APP_URL: http://localhost:3000 - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "unused" + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "secret-clerk-publishable-key" NEXT_PUBLIC_DISABLE_AUTH: "true" - NEXT_SERVER_API_URL: http://api:8000 + NEXT_SERVER_API_URL: http://localhost:8000 NODE_ENV: development with: context: frontend diff --git a/.github/workflows/test-pnpm-build.yml b/.github/workflows/test-pnpm-build.yml new file mode 100644 index 000000000..0b598917a --- /dev/null +++ b/.github/workflows/test-pnpm-build.yml @@ -0,0 +1,47 @@ +name: Test pnpm build + +on: + push: + branches: + - main + paths: + - "frontend/**" + - ".github/workflows/test-pnpm-build.yml" + pull_request: + branches: + - main + paths: + - "frontend/**" + - ".github/workflows/test-pnpm-build.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "22" + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: "latest" + + - name: Install dependencies + run: pnpm install + working-directory: ./frontend + + - name: Build project + env: + NEXT_PUBLIC_API_URL: http://localhost:3000 + NEXT_PUBLIC_APP_URL: http://localhost:3000 + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "secret-clerk-publishable-key" + NEXT_PUBLIC_DISABLE_AUTH: "true" + NEXT_SERVER_API_URL: http://localhost:8000 + NODE_ENV: development + run: pnpm build + working-directory: ./frontend diff --git a/.github/workflows/tests.yml b/.github/workflows/test-python.yml similarity index 97% rename from .github/workflows/tests.yml rename to .github/workflows/test-python.yml index 123a74771..cbd241c0c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/test-python.yml @@ -3,8 +3,12 @@ name: Tests on: push: branches: ["main"] + paths-ignore: + - "frontend/**" pull_request: branches: ["main"] + paths-ignore: + - "frontend/**" permissions: contents: read diff --git a/docker-compose.yml b/docker-compose.yml index fcc9c96b7..ec72ca4ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,17 @@ services: entrypoint: ["python", "tracecat/dsl/worker.py"] ui: - image: ghcr.io/tracecathq/tracecat-ui:${TRACECAT__IMAGE_TAG:-latest} + # image: ghcr.io/tracecathq/tracecat-ui:${TRACECAT__IMAGE_TAG:-latest} + build: + context: ./frontend + dockerfile: Dockerfile.prod + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL} + NEXT_PUBLIC_DISABLE_AUTH: ${TRACECAT__DISABLE_AUTH} + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} # Sensitive + NEXT_SERVER_API_URL: ${NEXT_SERVER_API_URL} + NODE_ENV: ${NODE_ENV} container_name: tracecat_ui ports: - 3000:3000 diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index 0b54cb017..6c2c644f0 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -1,3 +1,4 @@ +# https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile FROM node:22-alpine AS deps WORKDIR /app @@ -43,13 +44,23 @@ ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -# TODO: Automatically leverage output traces to reduce image size +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 -CMD ["/app/node_modules/.bin/next", "start"] +ENV PORT 3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD HOSTNAME="0.0.0.0" node server.js diff --git a/frontend/src/app/playbooks/page.tsx b/frontend/src/app/playbooks/page.tsx index 95c002db1..7f95d3f06 100644 --- a/frontend/src/app/playbooks/page.tsx +++ b/frontend/src/app/playbooks/page.tsx @@ -1,12 +1,12 @@ import React, { Suspense } from "react" import { CenteredSpinner } from "@/components/loading/spinner" -import { WorkflowsDashboard } from "@/components/playbooks/workflows-dashboard" +import { PlaybooksDashboard } from "@/components/playbooks/workflows-dashboard" export default async function Page() { return ( }> - + ) } diff --git a/frontend/src/components/dashboard/workflows-dashboard.tsx b/frontend/src/components/dashboard/workflows-dashboard.tsx index 7377b223d..5396720da 100644 --- a/frontend/src/components/dashboard/workflows-dashboard.tsx +++ b/frontend/src/components/dashboard/workflows-dashboard.tsx @@ -1,15 +1,16 @@ -import { Suspense } from "react" +"use client" + import Link from "next/link" import { ConeIcon } from "lucide-react" -import { fetchAllWorkflows } from "@/lib/workflow" +import { useWorkflows } from "@/lib/hooks" import { Button } from "@/components/ui/button" -import { Skeleton } from "@/components/ui/skeleton" import CreateWorkflowButton from "@/components/dashboard/create-workflow-button" import { WorkflowItem } from "@/components/dashboard/workflows-dashboard-item" import { AlertNotification } from "@/components/notifications" +import { ListItemSkeletion } from "@/components/skeletons" -export async function WorkflowsDashboard() { +export function WorkflowsDashboard() { return (
@@ -30,26 +31,22 @@ export async function WorkflowsDashboard() {
- - - - - - - } - > - - + ) } -export async function WorkflowList() { - const workflows = await fetchAllWorkflows() - if (workflows === null) { +export function WorkflowList() { + const { data: workflows, error, isLoading } = useWorkflows() + if (isLoading) { + return ( +
+ +
+ ) + } + if (error || workflows === undefined) { return ( ) @@ -59,20 +56,7 @@ export async function WorkflowList() {
{workflows.length === 0 ? (
-
- -
- - -
-
-
- -
- - -
-
+

Welcome to Tracecat 👋

diff --git a/frontend/src/components/playbooks/workflows-dashboard.tsx b/frontend/src/components/playbooks/workflows-dashboard.tsx index 4fb0fdf9f..b1d96b028 100644 --- a/frontend/src/components/playbooks/workflows-dashboard.tsx +++ b/frontend/src/components/playbooks/workflows-dashboard.tsx @@ -1,14 +1,15 @@ -import { Suspense } from "react" +"use client" + import Link from "next/link" import { InfoIcon } from "lucide-react" -import { fetchAllPlaybooks } from "@/lib/workflow" +import { usePlaybooks } from "@/lib/hooks" import { Button } from "@/components/ui/button" -import { Skeleton } from "@/components/ui/skeleton" import { AlertNotification } from "@/components/notifications" import { WorkflowItem } from "@/components/playbooks/workflows-dashboard-item" +import { ListItemSkeletion } from "@/components/skeletons" -export async function WorkflowsDashboard() { +export function PlaybooksDashboard() { return (
@@ -31,48 +32,31 @@ export async function WorkflowsDashboard() {
- - - - - -
- } - > - - +
) } -export async function WorkflowList() { - const workflows = await fetchAllPlaybooks() - if (workflows === null) { +export function PlaybookList() { + const { data: playbooks, error, isLoading } = usePlaybooks() + if (isLoading) { + return ( +
+ +
+ ) + } + if (error || playbooks === undefined) { return ( ) } return (
- {workflows.length === 0 ? ( + {playbooks.length === 0 ? (
-
- -
- - -
-
-
- -
- - -
-
+

No playbooks installed 😿

@@ -83,7 +67,7 @@ export async function WorkflowList() {

) : ( <> - {workflows.map((wf, idx) => ( + {playbooks.map((wf, idx) => ( ))} diff --git a/frontend/src/components/skeletons.tsx b/frontend/src/components/skeletons.tsx new file mode 100644 index 000000000..5438dba9a --- /dev/null +++ b/frontend/src/components/skeletons.tsx @@ -0,0 +1,20 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export function ListItemSkeletion({ n = 2 }: { n: number }) { + return ( + <> + {[...Array(n)].map((_i, idx) => ( +
+ +
+ + +
+
+ ))} + + ) +} diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 6b3ba4dbc..7415c62cb 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -2,7 +2,12 @@ import { useEffect, useState } from "react" import { useWorkflowBuilder } from "@/providers/builder" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { Action, CaseEvent, type Case } from "@/types/schemas" +import { + Action, + CaseEvent, + type Case, + type WorkflowMetadata, +} from "@/types/schemas" import { CaseEventParams, createCaseEvent, @@ -11,7 +16,12 @@ import { updateCase, } from "@/lib/cases" import { updateWebhook } from "@/lib/trigger" -import { getActionById, updateAction } from "@/lib/workflow" +import { + fetchAllPlaybooks, + fetchAllWorkflows, + getActionById, + updateAction, +} from "@/lib/workflow" import { toast } from "@/components/ui/use-toast" import { UDFNodeType } from "@/components/workspace/canvas/udf-node" @@ -226,3 +236,19 @@ export function useUpdateWebhook(workflowId: string) { return mutation } + +export function useWorkflows() { + const query = useQuery({ + queryKey: ["workflows"], + queryFn: fetchAllWorkflows, + }) + return query +} + +export function usePlaybooks() { + const query = useQuery({ + queryKey: ["playbooks"], + queryFn: fetchAllPlaybooks, + }) + return query +} diff --git a/frontend/src/lib/workflow.ts b/frontend/src/lib/workflow.ts index 290942502..996f1f7e8 100644 --- a/frontend/src/lib/workflow.ts +++ b/frontend/src/lib/workflow.ts @@ -59,7 +59,7 @@ export async function createWorkflow( return workflowMetadataSchema.parse(response.data) } -export async function fetchAllWorkflows(): Promise { +export async function fetchAllWorkflows(): Promise { try { const response = await client.get("/workflows") const workflows = response.data @@ -67,7 +67,7 @@ export async function fetchAllWorkflows(): Promise { return z.array(workflowMetadataSchema).parse(workflows) } catch (error) { console.error("Error fetching workflows:", error) - return null + throw error } } @@ -252,13 +252,13 @@ export async function copyPlaybook(workflowId: string) { * @param maybeToken * @returns */ -export async function fetchAllPlaybooks(): Promise { +export async function fetchAllPlaybooks(): Promise { try { const response = await client.get("/workflows?library=true") return response.data } catch (error) { console.error("Error fetching playbooks:", error) - return null + throw error } }