diff --git a/src/components/Learn/DocsQuickLinks.tsx b/src/components/Learn/DocsQuickLinks.tsx new file mode 100644 index 000000000..bcf889b57 --- /dev/null +++ b/src/components/Learn/DocsQuickLinks.tsx @@ -0,0 +1,101 @@ +import { Icon, type IconName } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Link } from "@/components/ui/link"; +import { Text } from "@/components/ui/typography"; +import { DOCUMENTATION_URL } from "@/utils/constants"; +import { tracking } from "@/utils/tracking"; + +interface QuickLink { + id: string; + label: string; + href: string; + icon: IconName; +} + +const QUICK_LINKS: QuickLink[] = [ + { + id: "getting-started", + label: "Getting started", + href: `${DOCUMENTATION_URL}getting-started/first-pipeline/`, + icon: "Rocket", + }, + { + id: "components", + label: "Components", + href: `${DOCUMENTATION_URL}core-concepts/what-are-components/`, + icon: "Package", + }, + { + id: "pipelines", + label: "Pipelines", + href: `${DOCUMENTATION_URL}core-concepts/understanding-inputs-outputs/`, + icon: "GitBranch", + }, + { + id: "secrets", + label: "Secrets & auth", + href: `${DOCUMENTATION_URL}core-concepts/secrets/`, + icon: "Lock", + }, + { + id: "runs", + label: "Running pipelines", + href: `${DOCUMENTATION_URL}core-concepts/tasks-and-executions/`, + icon: "Play", + }, + { + id: "schema", + label: "Schema reference", + href: `${DOCUMENTATION_URL}reference/schema/`, + icon: "Code", + }, +]; + +function QuickLinkPill({ link }: { link: QuickLink }) { + return ( + + + ); +} + +export function DocsQuickLinks() { + return ( + + + Quick links + + + {QUICK_LINKS.map((link) => ( + + ))} + + Full docs + + + + ); +} diff --git a/src/components/Learn/DocumentationPanel.tsx b/src/components/Learn/DocumentationPanel.tsx deleted file mode 100644 index dbe693dfe..000000000 --- a/src/components/Learn/DocumentationPanel.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Icon, type IconName } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Heading, Paragraph, Text } from "@/components/ui/typography"; -import { DOCUMENTATION_URL } from "@/utils/constants"; -import { tracking } from "@/utils/tracking"; - -interface QuickLink { - id: string; - label: string; - href: string; - icon: IconName; -} - -const QUICK_LINKS: QuickLink[] = [ - { - id: "getting-started", - label: "Getting started", - href: `${DOCUMENTATION_URL}getting-started/first-pipeline/`, - icon: "Rocket", - }, - { - id: "components", - label: "Components", - href: `${DOCUMENTATION_URL}core-concepts/what-are-components/`, - icon: "Package", - }, - { - id: "pipelines", - label: "Pipelines", - href: `${DOCUMENTATION_URL}core-concepts/understanding-inputs-outputs/`, - icon: "GitBranch", - }, - { - id: "secrets", - label: "Secrets & auth", - href: `${DOCUMENTATION_URL}core-concepts/secrets/`, - icon: "Lock", - }, - { - id: "runs", - label: "Running pipelines", - href: `${DOCUMENTATION_URL}core-concepts/tasks-and-executions/`, - icon: "Play", - }, - { - id: "schema", - label: "Schema reference", - href: `${DOCUMENTATION_URL}reference/schema/`, - icon: "Code", - }, -]; - -interface FaqItem { - id: string; - question: string; - answer: string; -} - -const FAQ_ITEMS: FaqItem[] = [ - { - id: "create-pipeline", - question: "How do I create my first pipeline?", - answer: - "Open the Pipelines tab and click 'New pipeline', or import one of the example pipelines from the Learning Hub to get started quickly.", - }, - { - id: "share-pipeline", - question: "Can I share a pipeline with someone else?", - answer: - "Yes — export the pipeline as YAML from the editor and share the file. The recipient can import it from the home dashboard.", - }, - { - id: "secrets", - question: "Where are secrets stored?", - answer: - "Secrets are managed under Settings → Secrets and are encrypted at rest. Components reference secrets by name at runtime.", - }, - { - id: "self-host", - question: "Can I run Tangle against my own backend?", - answer: - "Yes. Under Settings → Backend you can point Tangle at any compatible backend URL — see the docs for self-hosting instructions.", - }, -]; - -function QuickLinkTile({ link }: { link: QuickLink }) { - return ( - - -
- - ); -} - -function FaqRow({ item }: { item: FaqItem }) { - return ( - - - - {item.question} - - - - - {item.answer} - - - - ); -} - -export function DocumentationPanel() { - return ( - - - - - - Full docs ↗ - - - -
- {QUICK_LINKS.map((link) => ( - - ))} -
- - - - Frequently asked - - - {FAQ_ITEMS.map((item) => ( - - ))} - - -
- ); -} diff --git a/src/components/Learn/FaqPanel.tsx b/src/components/Learn/FaqPanel.tsx new file mode 100644 index 000000000..d811bfac6 --- /dev/null +++ b/src/components/Learn/FaqPanel.tsx @@ -0,0 +1,69 @@ +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Heading, Paragraph, Text } from "@/components/ui/typography"; +import { tracking } from "@/utils/tracking"; + +import faqItems from "./faq.json"; + +interface FaqItem { + id: string; + question: string; + answer: string; +} + +const FAQ_ITEMS = faqItems as FaqItem[]; + +function FaqRow({ item }: { item: FaqItem }) { + return ( + + + + {item.question} + + + + + {item.answer} + + + + ); +} + +export function FaqPanel() { + return ( + + + + + + {FAQ_ITEMS.map((item) => ( + + ))} + + + ); +} diff --git a/src/components/Learn/HelpCard.tsx b/src/components/Learn/HelpCard.tsx new file mode 100644 index 000000000..83ac6fc01 --- /dev/null +++ b/src/components/Learn/HelpCard.tsx @@ -0,0 +1,47 @@ +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Link } from "@/components/ui/link"; +import { Heading, Paragraph } from "@/components/ui/typography"; +import { GITHUB_DISCUSSIONS_URL, GIVE_FEEDBACK_URL } from "@/utils/constants"; +import { tracking } from "@/utils/tracking"; + +export function HelpCard() { + return ( +
+ + + + + {"Tangle is open source. Reach out, share an idea, or report a bug."} + + + + Ask the community + + + Report a bug + + + +
+ ); +} diff --git a/src/components/Learn/LearnSearchBar.tsx b/src/components/Learn/LearnSearchBar.tsx index 7ccbb19ea..7312ba3dd 100644 --- a/src/components/Learn/LearnSearchBar.tsx +++ b/src/components/Learn/LearnSearchBar.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Icon } from "@/components/ui/icon"; import { Input, InputGroup } from "@/components/ui/input"; +import { Text } from "@/components/ui/typography"; import { TANGLE_WEBSITE_URL } from "@/utils/constants"; import { tracking } from "@/utils/tracking"; @@ -17,24 +18,32 @@ export function LearnSearchBar() { return ( diff --git a/src/components/Learn/faq.json b/src/components/Learn/faq.json new file mode 100644 index 000000000..437ff2e96 --- /dev/null +++ b/src/components/Learn/faq.json @@ -0,0 +1,52 @@ +[ + { + "id": "create-pipeline", + "question": "How do I create my first pipeline?", + "answer": "Head to My Pipelines and click 'New pipeline', or import one of the example pipelines from the Learning Hub to get going in seconds." + }, + { + "id": "components", + "question": "What's the difference between a component and a task?", + "answer": "Components are reusable building blocks defined in YAML. Tasks are individual instances of a component placed on the canvas as part of a pipeline." + }, + { + "id": "share-pipeline", + "question": "Can I share a pipeline with someone else?", + "answer": "Yes. Export the pipeline as YAML from the editor and share the file. The recipient can import it from the home dashboard or open the import URL directly." + }, + { + "id": "secrets", + "question": "How do I use secrets in my pipeline?", + "answer": "Go to Settings, then Secrets, and click Add Secret with a name and value. In the pipeline editor, click the dynamic data dropdown (database icon) on any task argument field, select Secret, and choose it by name. The value is injected at runtime and never stored in the pipeline YAML." + }, + { + "id": "viewing-logs", + "question": "How do I view and debug logs for my pipeline components?", + "answer": "Select a task in the Pipeline Run view and click the Logs tab in the right panel to see error traces. If the Logs tab is empty, the container likely never started, so check the Details tab for error info. For subgraph nodes, double-click to navigate inside. Logs live on the inner tasks, not the parent." + }, + { + "id": "caching", + "question": "How does caching work, and how do I control it?", + "answer": "Tangle automatically caches task results based on the container spec and a hash of all inputs. If the same task with the same inputs has run before, the cached result is reused instantly. To disable caching for a task, select it, open the Configuration tab, and toggle caching off." + }, + { + "id": "runs", + "question": "How do I share, search, and navigate pipeline runs?", + "answer": "Select any task in a run and click the Copy node link button in the Arguments tab to get a shareable deep link to that specific node. Use the All Runs view in the Dashboard to search by pipeline name, creator, date range, or custom annotations." + }, + { + "id": "complex-pipelines", + "question": "How do I structure complex pipelines with subgraphs, branching, and conditional execution?", + "answer": "Pipelines are graph components, so you can nest one pipeline inside another as a subgraph. Export it as YAML and import it as a component. Tasks with no data dependency run in parallel automatically; tasks with dependencies run sequentially. Conditional if/else branching is not yet supported. If a task fails, only its downstream dependents are affected; other branches continue." + }, + { + "id": "manage-components", + "question": "How do I manage components: publishing, versioning, and reuse?", + "answer": "Components live in three places: User Components (browser-local, only you), Standard Library (built-in), and Published Components (shared workspace-wide). To publish, open the component info dialog from User Components, go to the Publish tab, and click Publish. Published components can't be deleted. Publish a new version instead." + }, + { + "id": "cancel-pipeline", + "question": "How do I cancel a pipeline?", + "answer": "In the Pipeline Run view, click the Cancel button. Only the run creator can cancel. Completed tasks keep their results; only pending and running tasks are terminated." + } +] diff --git a/src/routes/Dashboard/Learn/LearnHomeView.test.tsx b/src/routes/Dashboard/Learn/LearnHomeView.test.tsx index 74df084cd..4bf7014d0 100644 --- a/src/routes/Dashboard/Learn/LearnHomeView.test.tsx +++ b/src/routes/Dashboard/Learn/LearnHomeView.test.tsx @@ -34,7 +34,7 @@ describe("", () => { test("renders the search bar", () => { render(); expect( - screen.getByRole("textbox", { name: /search the learning hub/i }), + screen.getByRole("textbox", { name: /search the tangle docs/i }), ).toBeInTheDocument(); }); @@ -48,7 +48,14 @@ describe("", () => { ).toBeInTheDocument(); }); - test("renders the tip of the day, tours, examples and documentation sections", () => { + test("renders the docs quicklinks and full-docs link at the top", () => { + render(); + expect(screen.getByText("Getting started")).toBeInTheDocument(); + expect(screen.getByText("Schema reference")).toBeInTheDocument(); + expect(screen.getByText("Full docs")).toBeInTheDocument(); + }); + + test("renders the tip, tours, examples and FAQ sections", () => { render(); expect( screen.getByRole("heading", { level: 3, name: /tip of the day/i }), @@ -60,7 +67,7 @@ describe("", () => { screen.getByRole("heading", { level: 2, name: /example pipelines/i }), ).toBeInTheDocument(); expect( - screen.getByRole("heading", { level: 2, name: /documentation/i }), + screen.getByRole("heading", { level: 2, name: /frequently asked/i }), ).toBeInTheDocument(); }); }); diff --git a/src/routes/Dashboard/Learn/LearnHomeView.tsx b/src/routes/Dashboard/Learn/LearnHomeView.tsx index 2b13c4bcd..ab8f741fa 100644 --- a/src/routes/Dashboard/Learn/LearnHomeView.tsx +++ b/src/routes/Dashboard/Learn/LearnHomeView.tsx @@ -1,6 +1,8 @@ -import { DocumentationPanel } from "@/components/Learn/DocumentationPanel"; +import { DocsQuickLinks } from "@/components/Learn/DocsQuickLinks"; +import { FaqPanel } from "@/components/Learn/FaqPanel"; import { FeaturedExamples } from "@/components/Learn/FeaturedExamples"; import { FeaturedTours } from "@/components/Learn/FeaturedTours"; +import { HelpCard } from "@/components/Learn/HelpCard"; import { LearnPageHeader } from "@/components/Learn/LearnPageHeader"; import { LearnSearchBar } from "@/components/Learn/LearnSearchBar"; import { OnboardingHero } from "@/components/Learn/OnboardingHero"; @@ -16,7 +18,10 @@ export function LearnHomeView() { description="Everything you need to get the most out of Tangle, all in one place." icon="GraduationCap" /> - + + + + @@ -28,7 +33,14 @@ export function LearnHomeView() { - +
+
+ +
+
+ +
+
); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e6e78c78c..fadffff9c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,6 +7,10 @@ export const GIVE_FEEDBACK_URL = import.meta.env.VITE_GIVE_FEEDBACK_URL || "https://github.com/TangleML/tangle/issues"; +export const GITHUB_DISCUSSIONS_URL = + import.meta.env.VITE_GITHUB_DISCUSSIONS_URL || + "https://github.com/TangleML/tangle/discussions"; + export const PRIVACY_POLICY_URL = import.meta.env.VITE_PRIVACY_POLICY_URL || "https://tangleml.com/docs/privacy_policy/"; diff --git a/tsconfig.json b/tsconfig.json index b21322c90..df787e265 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, + "resolveJsonModule": true, /* Linting */ "skipLibCheck": true,