diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..f7a10ea --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ +/public/robots.txt +/public/sitemap.xml +/public/sitemap-*.xml + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/docs/.prettyierrc.yaml b/docs/.prettyierrc.yaml new file mode 100644 index 0000000..26e37f0 --- /dev/null +++ b/docs/.prettyierrc.yaml @@ -0,0 +1,6 @@ +tabWidth: 4 +useTabs: false +singleQuote: false +quoteProps: consistent +printWidth: 100 +endOfLine: lf diff --git a/docs/.stylelintrc.json b/docs/.stylelintrc.json new file mode 100644 index 0000000..243ec69 --- /dev/null +++ b/docs/.stylelintrc.json @@ -0,0 +1,7 @@ +{ + "extends": [ + "stylelint-config-standard", + "stylelint-config-clean-order/error", + "stylelint-prettier/recommended" + ] +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c25f794 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,50 @@ +# Overview + +* This is a static site for https://docs.yscope.com +* We use `npm` to develop/package the static site. + +# Contributing + +## Requirements + +Node.js via [prebuilt installers][nodejs-prebuilt-installer] / [nvm][nvm] / +[nvm-windows][nvm-windows]. + +## Setup + +Install the project's dependencies: + +```shell +npm install +``` + +## Running in development + +You can build and serve the viewer in debug mode using: + +```shell +npm run dev +``` + +The website should then be available at the URL by the command. + +## Building a distribution + +To create a build, run: + +```shell +npm run build +``` + +The build should then be available in the `out` directory. + +You can preview the build with [serve]: + +```shell +npx serve out +``` + +[nodejs-prebuilt-installer]: https://nodejs.org/en/download/prebuilt-installer +[nvm]: https://github.com/nvm-sh/nvm +[nvm-windows]: https://github.com/coreybutler/nvm-windows +[serve]: https://www.npmjs.com/package/serve \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 6be4ce5..0000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,21 +0,0 @@ -html[data-theme="dark"], html[data-theme="light"] { - --pst-color-primary: #3399ff; - --pst-color-secondary: #9580ff; -} - -a { - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -/* -Remove margin from sidebar-primary-items__end so that we don't have an unnecessary scrollbar. We're -not using the end items currently. -*/ -.bd-sidebar-primary .sidebar-primary-items__end { - margin-top: 0; - margin-bottom: 0; -} diff --git a/docs/app/assets/css/colors.css b/docs/app/assets/css/colors.css new file mode 100644 index 0000000..a42bd99 --- /dev/null +++ b/docs/app/assets/css/colors.css @@ -0,0 +1,85 @@ +/* CSS variables exported from Tailwind color config */ +:root { + --white: #ffffff; + --black: #000000; + + --light-bg: #ffffff; + --dark-bg: #16181b; + + --light-bg-alt: #efefef; + --dark-bg-alt: #111315; + + /* Brilliant Azure */ + --brilliant-azure: #1888FA; + --brilliant-azure-50: #e6f2fe; + --brilliant-azure-100: #cde5fe; + --brilliant-azure-150: #b5d9fd; + --brilliant-azure-200: #9bccfd; + --brilliant-azure-300: #69b2fc; + --brilliant-azure-400: #3799fb; + --brilliant-azure-500: #057ffa; + --brilliant-azure-550: #0573e1; + --brilliant-azure-600: #0466c8; + --brilliant-azure-700: #034c96; + --brilliant-azure-800: #023364; + --brilliant-azure-850: #02264B; + --brilliant-azure-900: #011932; + --brilliant-azure-950: #011223; + + /* Turquoise */ + --turquoise: #62D6BF; + --turquoise-50: #ebfaf7; + --turquoise-100: #d6f5ef; + --turquoise-200: #aeeade; + --turquoise-300: #85e0ce; + --turquoise-400: #5dd5bd; + --turquoise-500: #34cbad; + --turquoise-600: #2aa28a; + --turquoise-700: #18786f; + --turquoise-800: #094f4d; + --turquoise-850: #043C3B; + --turquoise-900: #002928; + --turquoise-950: #001e1d; + + /* Space Indigo */ + --space-indigo: #201E50; + --space-indigo-50: #ededf8; + --space-indigo-100: #dbdaf1; + --space-indigo-200: #b7b5e3; + --space-indigo-300: #9290d5; + --space-indigo-400: #6e6bc7; + --space-indigo-500: #4a46b9; + --space-indigo-600: #3b3894; + --space-indigo-700: #2c2a6f; + --space-indigo-800: #1e1c4a; + --space-indigo-900: #0f0e25; + --space-indigo-950: #0a0a1a; + + /* Dark Spruce */ + --dark-spruce: #2C5530; + --dark-spruce-50: #eef6ef; + --dark-spruce-100: #ddeedf; + --dark-spruce-200: #bcdcbf; + --dark-spruce-300: #9acb9f; + --dark-spruce-400: #78ba7f; + --dark-spruce-500: #57a85f; + --dark-spruce-600: #45874c; + --dark-spruce-700: #346539; + --dark-spruce-800: #234326; + --dark-spruce-900: #112213; + --dark-spruce-950: #0c180d; + + /* Tuscan Sun */ + --tuscan-sun: #F7C548; + --tuscan-sun-50: #fef7e7; + --tuscan-sun-100: #fdf0ce; + --tuscan-sun-200: #fbe09d; + --tuscan-sun-300: #f9d16c; + --tuscan-sun-400: #f7c23b; + --tuscan-sun-500: #f5b20a; + --tuscan-sun-600: #c48f08; + --tuscan-sun-700: #936b06; + --tuscan-sun-800: #624704; + --tuscan-sun-900: #312402; + --tuscan-sun-950: #221901; +} diff --git a/docs/app/assets/scss/styles.scss b/docs/app/assets/scss/styles.scss new file mode 100644 index 0000000..c8a19f1 --- /dev/null +++ b/docs/app/assets/scss/styles.scss @@ -0,0 +1,502 @@ +@import "tailwindcss"; +@import "../css/colors.css"; +@import "../../../node_modules/tw-animate-css/dist/tw-animate.css"; + +@import "../../../node_modules/bootstrap/scss/functions"; +@import "../../../node_modules/bootstrap/scss/variables"; +@import "../../../node_modules/bootstrap/scss/variables-dark"; +@import "../../../node_modules/bootstrap/scss/maps"; +@import "../../../node_modules/bootstrap/scss/mixins"; +@import "../../../node_modules/bootstrap/scss/utilities"; + +// Layout & components +@import "../../../node_modules/bootstrap/scss/root"; +@import "../../../node_modules/bootstrap/scss/reboot"; +@import "../../../node_modules/bootstrap/scss/type"; +@import "../../../node_modules/bootstrap/scss/containers"; +@import "../../../node_modules/bootstrap/scss/grid"; + +@import "../../../node_modules/bootstrap/scss/utilities/api"; + +/* Fade utility (used by modals, etc.) */ +.fade { + transition: opacity .15s linear; + opacity: 0; +} + +.fade.show { + opacity: 1; +} + +/* Collapse / Accordion basics: hide when not shown, simple height transition + for the JS-driven `.collapsing` state. */ +.collapse:not(.show) { + display: none; +} + +.collapse.show { + display: block; +} + +.collapsing { + height: 0; + overflow: hidden; + transition: height .35s ease; +} + +/* Navbar collapse uses the same show/hide behavior */ +.navbar-collapse.collapse:not(.show) { + display: none; +} + +.navbar-collapse.collapse.show { + display: block; +} + +/* Provide a small transition for accordion content where helpful */ +.accordion-collapse { + transition: height .35s ease; +} + + +// Themed GitHub button styles +.btn-github { + background: var(--white); + border: 1px solid #cccccc; + box-shadow: 0 2px 12px 0 rgba(0, 123, 255, 0.22); + border-radius: 8px; + color: #212529; + display: inline-flex; + align-items: center; + font-weight: 500; + gap: 0.35em; + padding: 0.18em 0.5em 0.18em 0.45em; + text-decoration: none; + transition: background 0.2s, color 0.2s, border 0.2s, box-shadow 0.2s; +} + +[data-bs-theme="dark"] .btn-github { + background: #212529; + color: var(--white); + border: 1px solid #444; +} + +[data-bs-theme="dark"] .btn-github svg { + color: var(--white); +} + +[data-bs-theme="light"] .btn-github svg { + color: #212529; +} + +:root { + --bs-body-bg: var(--white); + --bs-body-bg-rgb: 255, 255, 255; +} + +[data-bs-theme="dark"] { + --bs-body-bg: var(--dark-bg); //#212529; #0b0f13; #0E0D0E; + --bs-body-bg-rgb: 22, 24, 27; +} + +$yscope-colors: ( + "yscope-primary": #1888fa, + "yscope-secondary": #F7C548, +); +$theme-colors: map-merge($theme-colors, $yscope-colors); + +$yscope-colors-text: ( + "yscope-primary": #1888fa, + "yscope-secondary": black, +); +$yscope-colors-bg-subtle: ( + "yscope-primary": #1888fa, + "yscope-secondary": black, +); +$yscope-colors-border-subtle: ( + "yscope-primary": #1888fa, + "yscope-secondary": black, +); + +html { + font-family: var(--font-geist-sans), system-ui, -apple-system, "Segoe UI", sans-serif; + font-size: 18px; +} + +footer { + padding: 2rem 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-geist-sans), system-ui, -apple-system, "Segoe UI", sans-serif; +} + +.main-chart { + text-align: center; + padding: 0px 20px; +} + +.section { + padding: 30px 0; + + h1 { + font-size: 1.6rem; + } + + h2 { + font-size: 1.5rem; + } + + .subtitle { + font-size: 1rem; + font-style: italic; + } +} + + +@media (min-width: 576px) { + .section { + h1 { + font-size: 2rem; + } + + .subtitle { + font-size: 1.3rem; + } + } +} + + +.theme-switcher-icon::before { + /* removed bootstrap-icons dependency; theme switcher uses JS-rendered SVG icons now */ + display: none; +} + +.btn-yscope-secondary { + border: solid 1px #cccccc; + border-radius: 8px; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out; +} + +[data-bs-theme="light"] .btn-yscope-secondary { + color: #3f3f3f; + border: solid 1px #cccccc; +} + +[data-bs-theme="dark"] .btn-yscope-secondary { + color: #c2c2c2; + border: solid 1px #444; +} + +.btn-yscope-secondary:hover { + color: var(--black); + background-color: var(--tuscan-sun); +} + +[data-bs-theme="light"] { + footer { + border-top: #f2f2f2 solid 2px; + } + + .theme-switcher-icon::before { + content: "\f497"; + } +} + +[data-bs-theme="dark"] { + footer { + border-top: #212529 solid 2px; + } + + .btn-yscope-secondary { + --bs-btn-color: var(--white); + } + + .theme-switcher-icon::before { + content: "\f5a2"; + } +} + +.logo { + color: var(--brilliant-azure); + font-family: 'Abeezee', abeezee, sans-serif; + font-weight: 700; + font-size: 2.2rem; +} + +.logo-color { + color: var(--brilliant-azure); +} + +.checkmark-list { + list-style: none; + padding: 0; + margin: 0; + + li { + position: relative; + padding-left: 1.8rem; + padding-bottom: 5px; + padding-top: 5px; + } + + li::before { + /* replaced icon font glyph with a unicode checkmark to avoid bootstrap-icons dependency */ + content: "\2713"; + color: var(--brilliant-azure); + position: absolute; + left: 0; + top: 0.15rem; + font-weight: 600; + font-size: 1rem; + } + + font-size: 1.1rem; +} + +.policy-list { + list-style: none; + padding: 0; + margin: 0; + + a { + color: inherit; + text-decoration: none; + } +} + +.main-section h2 { + font-weight: bold; +} + +.accordion-button { + font-weight: 600; +} + +/* Theme-aware styles for the nav bar border */ +.nav-border { + border-bottom: #f2f2f2 solid 2px; +} + +[data-bs-theme="light"] .nav-border { + border-bottom: #f2f2f2 solid 2px; +} + + +[data-bs-theme="dark"] .nav-border { + border-bottom: #212529 solid 2px; +} + +/* Theme-aware styles for the navlink button variant */ +.nav-link-btn { + transition: color 0.15s ease-in-out; +} + +[data-bs-theme="light"] .nav-link-btn { + color: #717171; +} + +[data-bs-theme="dark"] .nav-link-btn { + color: #a2a4a5; +} + +.nav-link-btn:hover { + color: var(--brilliant-azure); +} + +.nav-link-btn-alt { + transition: color 0.15s ease-in-out; +} + +[data-bs-theme="light"] .nav-link-btn-alt { + color: var(--brilliant-azure-400); +} + +[data-bs-theme="dark"] .nav-link-btn-alt { + color: var(--brilliant-azure-600); +} + +.footer .nav-link-btn:hover, +.footer .nav-link-btn-alt:hover { + color: var(--tuscan-sun); +} + + +/* Theme-aware styles for the theme toggle button variant */ +.theme-toggle-btn { + border: solid 1px #cccccc; + border-radius: 8px; + color: #a2a4a5; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out; +} + +[data-bs-theme="light"] .theme-toggle-btn { + color: #717171; + border: solid 1px #cccccc; +} + +[data-bs-theme="light"] .theme-toggle-btn:hover { + color: var(--white); + background-color: var(--space-indigo); +} + +[data-bs-theme="dark"] .theme-toggle-btn { + color: #a2a4a5; + background-color: #212529; + border: solid 1px #444; +} + +[data-bs-theme="dark"] .theme-toggle-btn:hover { + color: var(--black); + background-color: var(--tuscan-sun); +} + +.dev-docs-btn { + background-color: var(--brilliant-azure-550) !important; + border-radius: 8px; + color: var(--white); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out; +} + +.dev-docs-btn:hover { + background-color: var(--brilliant-azure-700) !important; +} + +.mobile-menu-btn { + border-radius: 8px; +} + +/* Theme-aware styles for the mobile menu sheet */ +.mobile-menu-sheet { + background-color: var(--white); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out; +} + +[data-bs-theme="light"] .mobile-menu-sheet { + background-color: var(--white); +} + +[data-bs-theme="dark"] .mobile-menu-sheet { + background-color: #1A1D20; +} + +/* Map legacy `.container` class to Tailwind's container utility so existing markup keeps working */ +.container { + @apply container mx-auto; +} + +.container-fluid { + width: 100%; + @apply px-4; +} + +/* Override Bootstrap container gutter so Tailwind padding wins on large screens */ +.container { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +@media (min-width: 1140px) { + .container { + padding-left: 5rem; + padding-right: 5rem; + } +} + +@media (min-width: 1420px) { + .container { + padding-left: 10rem; + padding-right: 10rem; + } +} + +.newsletter-card { + background-color: var(--light-bg-alt); + border: 1px solid #cccccc; + border-radius: 8px; + padding: 20px; + text-align: center; +} + +[data-bs-theme="dark"] .newsletter-card { + background-color: var(--dark-bg-alt); + border: 1px solid #444; +} + +.newsletter-card .text-input { + background-color: var(--white); + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + width: 200px; +} + +[data-bs-theme="dark"] .newsletter-card .text-input { + background-color: var(--dark-bg); + border: 1px solid #444; + color: var(--white); +} + +.homepage_icons_category { + display: flex; + flex-flow: row wrap; + row-gap: 0.5rem; + justify-content: space-between; +} + +.homepage_icon { + width: 36px; + height: 36px; +} + +.homepage_icon_box { + width: 56px; + height: 56px; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.homepage_icon_row { + width: 245px; +} + +[data-bs-theme="light"] .homepage_icon_box { + background-color: #f8f8f8; + border: 1px solid #e8e8e8; +} + +[data-bs-theme="dark"] .homepage_icon_box { + background-color: #555e6930; + border: 1px solid #555e69; +} + +/* Stack welcome + buttons for screens < 1100px */ +@media (max-width: 1100px) { + .homepage-hero-row { + flex-direction: column; + align-items: center; + } + + .homepage-hero-row > .col { + width: 100%; + max-width: 100%; + } + + .homepage-hero-row > .col.my-4 { + margin-top: 1rem; + } +} + +/* Stack welcome + buttons for screens < 500px */ +@media (max-width: 500px) { + .homepage-hero-title{ + font-size: 25px; + } +} \ No newline at end of file diff --git a/docs/app/content.ts b/docs/app/content.ts new file mode 100644 index 0000000..0f87c43 --- /dev/null +++ b/docs/app/content.ts @@ -0,0 +1,144 @@ +export const getCategories = (prestoSrc: string, mcpSrc: string) => { + return [ + { + title: "Deploy CLP", + items: [ + { + href: "/clp/main/user-docs/quick-start/index", + imgAlt: "Single-node", + imgSrc: "/assets/images/single-node.svg", + label: "Single Node", + }, + { + href: "/clp/main/user-docs/guides-docker-compose-deployment.html", + imgAlt: "Docker Compose", + imgSrc: "/assets/images/docker-compose_icon.svg", + label: "Docker Compose", + }, + { + href: "/clp/main/user-docs/guides-k8s-deployment.html", + imgAlt: "Kubernetes", + imgSrc: "/assets/images/kubernetes.svg", + label: "Kubernetes", + }, + ], + }, + { + title: "Log Input", + items: [ + { + href: "/clp/main/user-docs/guides-using-object-storage/index", + imgAlt: "S3", + imgSrc: "/assets/images/s3.svg", + label: "S3", + }, + { + external: true, + href: "https://github.com/y-scope/clp-loglib-py", + imgAlt: "Python Library", + imgSrc: "/assets/images/python.svg", + label: "Python", + }, + { + href: "/clp/main/user-docs/guides-using-log-ingestor.html", + imgAlt: "Log Ingestor", + imgSrc: "/assets/images/log-ingestor.svg", + label: "Log Ingestor", + }, + ], + }, + { + title: "Analyze & View", + items: [ + { + href: "/clp/main/user-docs/guides-using-presto.html", + imgAlt: "Presto", + imgSrc: prestoSrc, + imgStyle: {transform: "scale(1.2)", transformOrigin: "center"}, + label: "Presto", + }, + { + href: "/clp/main/user-docs/guides-mcp-server/index.html", + imgAlt: "MCP", + imgSrc: mcpSrc, + label: "MCP Server", + }, + { + href: "/clp/main/user-docs/guides-using-the-api-server.html", + imgAlt: "API Server", + imgSrc: "/assets/images/api-server.svg", + label: "API Server", + }, + { + href: "/yscope-log-viewer/main/", + imgAlt: "Log viewer", + imgSrc: "/assets/images/log-viewer_icon.svg", + label: "Log Viewer", + }, + { + href: "/clp/main/user-docs/reference-json-search-syntax", + imgAlt: "JSON Search", + imgSrc: "/assets/images/json.svg", + label: "JSON Search", + }, + { + href: "/clp/main/user-docs/reference-text-search-syntax", + imgAlt: "Text Search", + imgSrc: "/assets/images/text.svg", + label: "Text Search", + }, + ], + }, + { + title: "Resources", + items: [ + { + href: "/clp/main/user-docs/resources-datasets.html", + imgAlt: "Datasets", + imgSrc: "/assets/images/datasets.svg", + label: "Datasets", + }, + { + external: true, + href: "https://benchmarks.yscope.com/log-archival-bench/", + imgAlt: "Benchmarks", + imgSrc: "/assets/images/benchmarks.svg", + label: "Benchmarks", + }, + { + external: true, + href: "https://blog.yscope.com/", + imgAlt: "Blog", + imgSrc: "/assets/images/blog.svg", + label: "Blog", + }, + ], + }, + { + title: "References", + items: [ + { + href: "/clp/main/user-docs/reference-sbin-scripts/index", + imgAlt: "Package Scripts", + imgSrc: "/assets/images/scripts.svg", + label: "Package Scripts", + }, + { + href: "/clp/main/user-docs/reference-unstructured-schema-file", + imgAlt: "Schema File Syntax", + imgSrc: "/assets/images/schema-file-syntax.svg", + label: "Schema File Syntax", + }, + { + external: true, + href: "https://www.yscope.com/publications", + imgAlt: "Publications", + imgSrc: "/assets/images/publications.svg", + label: "Publications", + }, + ], + }, + ]; +}; + +export type Category = {title: string; items: Array}; diff --git a/docs/app/dev-guide/contrib-guides-cpp/layout.tsx b/docs/app/dev-guide/contrib-guides-cpp/layout.tsx new file mode 100644 index 0000000..9d7e1c3 --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-cpp/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "C++ Contirbution | YScope Developer Guide", + description: "C++ contribution guidelines and integration notes for YScope components " + + "written in C++.", +}; + +/** + * Layout wrapper for the C++ contribution guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const CppLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default CppLayout; diff --git a/docs/app/dev-guide/contrib-guides-cpp/page.tsx b/docs/app/dev-guide/contrib-guides-cpp/page.tsx new file mode 100644 index 0000000..c2caf2a --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-cpp/page.tsx @@ -0,0 +1,339 @@ +/* eslint-disable max-lines */ +/* eslint-disable max-lines-per-function */ +"use client"; +import Admonition from "../../shared/Admonition"; +import CodeBlock from "../../shared/CodeBlock"; + + +/** + * C++ contribution and style guidelines page component. + * + * @return The C++ guidelines page component. + */ +const CppGuidelines = () => { + return ( +
+
+

C++

+

Follow the guidelines below when writing and updating C++ files.

+
+

Automated linting

+

+ As mentioned in the overview, where possible, we have automated linting + processes, so you need not remember all the guidelines perfectly. For C++, we + currently use the following tools: +

+ +
    +
  • + clang-format + {" "} + for formatting ( + base config + {""} + ) +
  • +
  • + clang-tidy + {" "} + for static analysis ( + base config + {""} + ) +
  • +
+ +

+ When the linter disagrees with a guideline, err on the side of following the + linter, since: +

+
    +
  • + we don't want automated runs of the linters to fail, leading to them + being ignored due to the noise; +
  • +
  • the linter may have a good reason for disagreeing with the guideline;
  • +
  • + the linter's configuration may be more up-to-date than the guideline. +
  • +
+ + +

+ To learn about how to apply the linters to a new C++ project, see + {" "} + adding-cpp-linting + {" "} + . +

+
+ +

+ When you encounter such a disagreement, if it hasn't been noted below, + please open an + {" "} + issue + {" "} + to track it. +

+
+

Guidelines

+

+ We adhere to + {" "} + Google's C++ style guide + {" "} + (as of + {" "} + 8f97e24 + {" "} + ) with the following exceptions (organized according to the sections in + Google's style guide). +

+ + +

+ This section is a work in progress and does not yet include all + exceptions after the Classes section of Google's style guide. +

+
+
+ +

Header files

+
+

Self-contained Headers

+
    +
  • + Header files should end in + {" "} + .hpp + {" "} + . +
  • +
  • + Don't use non-header files meant for inclusion (e.g., + {" "} + .impl + {" "} + / + .inc + {" "} + / + .tpp + {" "} + files included at the end of the + .hpp + {" "} + file), since they can confuse static analysis tools. +
  • +
+
+ +

The #define Guard

+

The symbol name should have the form

+ __"} + language={"text"} + showCopy={true}/> + +

where:

+
    +
  • + <NAMESPACE> + {" "} + is the namespace of the file. For files in a nested namespace, each + namespace layer should be separated by an underscore. +
  • +
  • + <FILENAME_STEM> + {" "} + is the file's name without the extension. For stems with multiple + words, the words should + {" "} + not + {" "} + be separated with underscores. +
  • +
  • + <FILENAME_EXTENSION> + {" "} + is the file's extension. +
  • +
+ +

For example:

+

+ clp/streaming_archive/reader/SegmentManager.hpp +

+ + +
+

Names and order of includes

+
    +
  • + For codebases where the code is not organized into well-defined libraries, + it is fine to use UNIX directory aliases to include headers. +
  • +
  • + For C headers that have C++ counterparts (e.g., + {" "} + stddef.h + {" "} + vs + {" "} + cstddef + {" "} + ), use the C++ counterpart. +
  • +
+ +
+

Scoping

+
+

Namespaces

+

+ Single-line nested namespace declarations (e.g. + {" "} + bar + {" "} + in + {" "} + foo + {" "} + ) should use the following format (unless doing so would affect clarity): +

+ + +

Internal linkage

+

+ Only use unnamed namespaces (instead of the + {" "} + static + {" "} + qualifier) to give functions and variables internal linkage. However, as + Google's style guide indicates, you can't use unnamed namespaces in + header files. For symbols that should only be used within a header file, you + can create a named namespace in the header file, where its name is of the form: +

+ _internal"} + language={"text"} + showCopy={true}/> + +

where:

+
    +
  • + <FilenameStem_stem> + {" "} + is the file’s name without the extension. +
  • +
  • + _internal + {" "} + is a fixed suffix indicating the namespace’s purpose. +
  • +
+ +

For example:

+ + clp/streaming_archive/reader/SegmentManager.hpp + + + + +
+

Classes

+
+

Doing work in constructors

+

+ We allow (but discourage) the use of exceptions, even in constructors. If + creating an object can fail, you're encouraged to use a factory function + that performs the work that can fail, and then returns a result containing an + error code if unsuccessful or the object if successful. +

+ +

Declaration order

+

Within each section, order declarations as follows:

+
    +
  1. + Types and type aliases ( + {""} + typedef + {""} + , + {" "} + using + {""} + , + {" "} + enum + {""} + , nested structs and classes, and + {" "} + friend + {" "} + types). +
  2. +
  3. Static constants.
  4. +
  5. + Static functions: +
      +
    • Factory functions.
    • +
    • Other functions.
    • +
    +
  6. +
  7. Static variables.
  8. +
  9. Constructors.
  10. +
  11. Copy & move constructors and assignment operators.
  12. +
  13. The destructor.
  14. +
  15. + Methods (member functions): +
      +
    • Overridden methods.
    • +
    • Implemented abstract methods.
    • +
    • All other methods.
    • +
    +
  16. +
  17. Data members.
  18. +
+ +

+ The differences between our declaration order and the order in Google's + style guide is to conform with our general + {" "} + ordering guidelines + {" "} + . +

+
+
+ ); +}; + +export default CppGuidelines; diff --git a/docs/app/dev-guide/contrib-guides-general/layout.tsx b/docs/app/dev-guide/contrib-guides-general/layout.tsx new file mode 100644 index 0000000..4cf3286 --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-general/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "General Contributor Guidelines | YScope Developer Guide", + description: "General contribution guidelines for YScope: coding standards, PR process, and " + + "review expectations.", +}; + +/** + * Layout wrapper for the general contribution guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const GeneralGuidelinesLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default GeneralGuidelinesLayout; diff --git a/docs/app/dev-guide/contrib-guides-general/page.tsx b/docs/app/dev-guide/contrib-guides-general/page.tsx new file mode 100644 index 0000000..1a2275a --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-general/page.tsx @@ -0,0 +1,36 @@ +"use client"; + +/** + * General guidelines page component. + * + * @return The rendered general guidelines section. + */ +const GeneralGuidelines = () => { + return ( +
+
+

General guidelines

+

Follow the guidelines below when writing and updating any source files.

+
+

Code organization

+
+

Declaration order

+

Organize declarations in order of:

+
    +
  1. Visibility, from public to private
  2. +
  3. Lifetime, from static to dynamic
  4. +
  5. Alphabetically (unless a different ordering is required)
  6. +
+ +

+ This is so readers who read from top to bottom should understand the high- + level interfaces (public/static) before the low-level implementation + details (private/dynamic). Alphabetical ordering is to make symbols easy + to find. +

+
+
+ ); +}; + +export default GeneralGuidelines; diff --git a/docs/app/dev-guide/contrib-guides-overview/layout.tsx b/docs/app/dev-guide/contrib-guides-overview/layout.tsx new file mode 100644 index 0000000..4ad62f8 --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-overview/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "Contribution Overview | Yscope Developer Guide", + description: "Overview of contribution guidelines for Yscope: processes, expectations, and " + + "best practices.", +}; + +/** + * Layout wrapper for the developer guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const ContributionOverviewLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default ContributionOverviewLayout; diff --git a/docs/app/dev-guide/contrib-guides-overview/page.tsx b/docs/app/dev-guide/contrib-guides-overview/page.tsx new file mode 100644 index 0000000..7ae652c --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-overview/page.tsx @@ -0,0 +1,45 @@ +"use client"; +import Admonition from "../../shared/Admonition"; + + +/** + * Overview page component that displays contribution guideline links. + * + * @return The Overview page component. + */ +const Overview = () => { + return ( +
+
+

Overview

+

+ This section contains contribution guidelines for different languages and + tools. Where possible, we have automated linting processes so that you + don't need to read and remember dozens of guidelines. Thus, the + guidelines here are only those for which we cannot or have not yet + automated. +

+ +

+ This section is a work in progress while we open-source all our guidelines. +

+
+ +

Guides

+ +
+
+ ); +}; + +export default Overview; diff --git a/docs/app/dev-guide/contrib-guides-taskfiles/layout.tsx b/docs/app/dev-guide/contrib-guides-taskfiles/layout.tsx new file mode 100644 index 0000000..266cc89 --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-taskfiles/layout.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "Taskfiles | YScope Developer Guide", + description: "Guidelines for writing and maintaining Taskfiles used across YScope projects.", +}; + +/** + * Layout wrapper for the taskfiles contribution guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const TaskfilesLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default TaskfilesLayout; diff --git a/docs/app/dev-guide/contrib-guides-taskfiles/page.tsx b/docs/app/dev-guide/contrib-guides-taskfiles/page.tsx new file mode 100644 index 0000000..de916f9 --- /dev/null +++ b/docs/app/dev-guide/contrib-guides-taskfiles/page.tsx @@ -0,0 +1,773 @@ +/* eslint-disable max-lines */ +/* eslint-disable max-lines-per-function */ +"use client"; +import Admonition from "../../shared/Admonition"; +import CodeBlock from "../../shared/CodeBlock"; + + +/** + * Taskfiles page React component that renders the Taskfiles contribution guide. + * + * @return The rendered Taskfiles page. + */ +const Taskfiles = () => { + return ( +
+
+

Taskfiles

+ +

+ Follow the guidelines below when writing and updating + {" "} + Taskfiles + {""} + . Note that neither the guidelines nor examples are written as a rigorous + specification, but they should be sufficient to understand the spirit of + each guideline. +

+ +

Variable naming

+
    +
  1. + Variables should be named using + {" "} + SCREAMING_SNAKE_CASE + {""} + . +
  2. +
  3. + Variables in the global scope (as opposed to a task's scope) should + be prefixed with + {" "} + G_ + {" "} + to avoid conflicts with local variables. +
  4. +
+ +

+ Example +

+ + +
+

Paths

+
+

Matching files with glob patterns

+

Use the glob patterns in the table below depending on your use case.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Use casePattern
Match all files in the current directory. + * +
+ Match all files with the + {" "} + .x + {" "} + extension in the current directory. + + *.x +
+ Match all files that are in a subdirectory directly below the + current directory. + + */* +
+ Match all files with the + {" "} + .x + {" "} + extension that are in a subdirectory directly below the current + directory. + + */*.x +
+ Match all files in the current directory and all levels of + subdirectories. + + **/* +
+ Match all files with the + {" "} + .x + {" "} + extension in the current directory and all levels of + subdirectories. + + **/*.x +
+ + +

+ Don't use the pattern + {" "} + ** + {" "} + (instead of + {" "} + **/* + ) since it is equivalent to + {" "} + * + . Note that this isn't the case in bash. +

+
+ +
+

Paths in commands

+

All paths used in commands should be quoted.

+
+

+ Example +

+ "{{.FILE_PATH}}" +` + }/> + +

Built-in variables

+
    +
  1. + Don't use the variable + {" "} + + {"{{.ROOT_TASKFILE}}"} + +
      +
    • + v3.35.1 has a bug that makes it equivalent to + {" "} + ROOT_DIR + {" "} + (i.e., the parent directory of the root Taskfile, rather than + the path to the root Taskfile). +
    • +
    +
  2. +
  3. + When using the variable + {" "} + + {"{{.TASK}}"} + + {" "} + in a task within a namespace (e.g., it's in a nested Taskfile), + replace the + {" "} + : + {" "} + in the name with + {" "} + # + {" "} + as follows: + {" "} + + {"{{.TASK | replace "} + \“:\” \“#\” + {"}}"} + + . +
      +
    • This ensures the task name can be used as a filename.
    • +
    +
  4. +
+ +
+

+ sources + {" "} + and + {" "} + generates +

+

+ The task attributes + {" "} + sources + {" "} + and + {" "} + generates + {" "} + are supposed to allow us to control whether a task is + run based on whether its source files or generated files have changed, but + {" "} + task + {""} + 's behaviour may + seem unintuitive. So to understand the guidelines, you'll first need + to understand + {" "} + task + {""} + 's + behaviour. +

+ +

+ task + {" "} + has two methods to track changes to source files, specified using the + {" "} + method + {" "} + attribute: + {" "} + checksum + {" "} + which tracks changes to the source files checksums and + {" "} + timestamp + {" "} + which tracks changes to the source files' modification timestamps. + Note that checksums and modification timestamps are only tracked for + source files, + {" "} + not + {" "} + generated files. +

+ +

+ If a task has a + {" "} + sources + {" "} + attribute, then the task will run if: +

+
    +
  • + it has never been run (the checksums/modification timestamps of the + source files have not been cached), or +
  • +
  • the content of any listed source file has changed.
  • +
+ +

+ This is true even if a + {" "} + sources + {" "} + entry is a glob that matches one or more file paths. +

+ +

+ If the task has a + {" "} + sources + {" "} + and + {" "} + a + {" "} + generates + {" "} + attribute, the task will run if any source file has changed or if any + {" "} + generates + {" "} + entry doesn't exist. Note however, that: +

+
    +
  • + task + {" "} + doesn't checksum the generated files nor check if they are older + than the source files; it only checks for existence. +
  • +
  • + When using globs, + {" "} + task + {" "} + only checks whether the glob entry is satisfied rather than whether all + previously generated files exist. +
      +
    • + E.g., if the + {" "} + generates + {" "} + entry is + {" "} + build/nodejs/node/**/* + {""} + , + {" "} + task + {" "} + will re-run the task if the + {" "} + node + {" "} + or + {" "} + nodejs + {" "} + directory don't exist, or if they're empty; but + {" "} + task + {" "} + won't re-run the task if all but one file inside + {" "} + node + {" "} + exists. +
    • +
    +
  • +
+ +

+ Overall, this means that + {" "} + task + {" "} + will not detect + {" "} + all + {" "} + changes to the generated files. +

+ +
+

Depending on generated files

+

+ Every task with a + {" "} + sources + {" "} + field should depend on the generated files of its dependencies. +

+ + + +
+

+ generates + {" "} + and glob patterns +

+

+ Don't use + {" "} + generates + {" "} + entries with glob patterns unless you've accounted for the limitations + above. Instead, you can manually checksum the generated files using the + utility tasks below: +

+ + - + --group=0 + --mtime='UTC 1970-01-01' + --numeric-owner + --owner=0 + --sort=name + +compute-checksum: + desc: "Tries to compute a checksum for the given directory and output it to a file." + internal: true + silent: true + requires: + vars: ["DATA_DIR", "OUTPUT_FILE"] + cmds: + - >- + tar cf - + --directory "{{.DATA_DIR}}" + --group=0 + --mtime='UTC 1970-01-01' + --numeric-owner + --owner=0 + --sort=name + {{.CHECKSUM_TAR_BASE_ARGS}} . 2> /dev/null + | md5sum > {{.OUTPUT_FILE}} + ignore_error: true + +validate-checksum: + desc: "Validates the checksum of the given directory matches the checksum in the given file, or deletes the checksum file otherwise." + internal: true + silent: true + requires: + vars: ["CHECKSUM_FILE", "DATA_DIR"] + vars: + TMP_CHECKSUM_FILE: "{{.CHECKSUM_FILE}}.tmp" + cmds: + - task: "compute-checksum" + vars: + DATA_DIR: "{{.DATA_DIR}}" + OUTPUT_FILE: "{{.TMP_CHECKSUM_FILE}}" + - defer: "rm -f '{{.TMP_CHECKSUM_FILE}}'" + - >- + ( + test -d "{{.DATA_DIR}}" && diff -q '{{.TMP_CHECKSUM_FILE}}' '{{.CHECKSUM_FILE}}' 2> /dev/null + ) || rm -f '{{.CHECKSUM_FILE}}' +` + }/> + +

You can use the utility tasks as follows:

+ + +

Thus, the task will re-run if either:

+
    +
  • the source files change;
  • +
  • the checksum file doesn't exist; or
  • +
  • + validate-checksum + {" "} + fails because a generated file was changed. +
  • +
+ +
+

Task ordering

+

1. All non-internal tasks should appear before internal tasks.

+

+   + Example +

+ + +

+ 2. Within the internal or non-internal group of tasks, tasks should be + organized either alphabetically or following some logical ordering. +

+ +
+

Ordering of task attributes

+

+ Task attributes should be ordered first by the categories we define below, + and then alphabetically within each category. The one exception to the + alphabetical ordering are the execution control attributes. +

+ +
    +
  • + Descriptors +
      +
    • + aliases +
    • +
    • + desc +
    • +
    • + internal +
    • +
    • + summary +
    • +
    +
  • +
  • + Logging +
      +
    • + label +
    • +
    • + prefix +
    • +
    • + silent +
    • +
    +
  • +
  • + Variables +
      +
    • + dotenv +
    • +
    • + env +
    • +
    • + vars +
    • +
    +
  • +
  • + Inputs +
      +
    • + requires +
    • +
    • + sources +
    • +
    +
  • +
  • + Environment control +
      +
    • + dir +
    • +
    • + platforms +
    • +
    • + set +
    • +
    • + shopt +
    • +
    +
  • +
  • + Outputs +
      +
    • + generates +
    • +
    • + method +
    • +
    +
  • +
  • + Execution control (ordered by execution order) +
      +
    • + prompt +
    • +
    • + run +
    • +
    • + deps +
    • +
    • + preconditions +
    • +
    • + status +
    • +
    • + cmds +
    • +
    • + ignore_error +
    • +
    • + interactive +
    • +
    +
  • +
+ +
+

Ordering of list/map entries

+

+ List/map entries should be ordered alphabetically (except for + {" "} + vars + {""} + ), case-insensitively. Where there are several attributes, they may be + broken down into categories with a comment above each category. +

+ +

+ Entries in the + {" "} + vars + {" "} + attribute may violate this ordering so that variables defined later can + depend on the values of variables defined earlier. +

+ +

+ Example +

+ + +
+

Task docstrings

+

+ For reusable utility tasks that take arguments, you should add a docstring + above the task definition using a JSDoc-like syntax. The following example + demonstrates the syntax: +

+ + + +
    +
  • + The task's description (and the empty line following it) may be + omitted if the task's name makes it obvious. +
  • +
  • + Each parameter's description may be omitted if it's obvious + (however, the tag, type, and name should always be present). +
  • +
  • + Each parameter's type should use Go's (rather than + JavaScript's) syntax for specifying types. +
  • +
+ + +

+ Tasks have an optional + {" "} + desc + {" "} + attribute that technically serves the same purpose as the task description in the docstring. However, the description in the docstring can be easier to format, and we recommend against using + {" "} + desc + {" "} + when a docstring is present. +

+
+
+
+ ); +}; + +export default Taskfiles; diff --git a/docs/app/dev-guide/layout.tsx b/docs/app/dev-guide/layout.tsx new file mode 100644 index 0000000..c217d8b --- /dev/null +++ b/docs/app/dev-guide/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "YScope — Developer Guide", + description: "Developer guide for YScope: contribution guidelines, integrations, taskfiles, " + + "and deployment.", +}; + +/** + * Layout wrapper for the developer guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const DevGuideLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default DevGuideLayout; diff --git a/docs/app/dev-guide/misc-deploying/layout.tsx b/docs/app/dev-guide/misc-deploying/layout.tsx new file mode 100644 index 0000000..eaa2b05 --- /dev/null +++ b/docs/app/dev-guide/misc-deploying/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type {Metadata} from "next"; + + +export const metadata: Metadata = { + title: "Deploying Main Docs | YScope Developer Guide", + description: "Instructions for deploying the main documentation site for YScope and related " + + "projects.", +}; + +/** + * Layout wrapper for the deploying main docs guide pages. + * + * @param root0 Component props. + * @param root0.children Page content to render. + * @return The children elements to render. + */ +const DeployingMainDocsLayout = ({children}: {children: React.ReactNode}) => { + return children; +}; + +export default DeployingMainDocsLayout; diff --git a/docs/app/dev-guide/misc-deploying/page.tsx b/docs/app/dev-guide/misc-deploying/page.tsx new file mode 100644 index 0000000..3fd3620 --- /dev/null +++ b/docs/app/dev-guide/misc-deploying/page.tsx @@ -0,0 +1,143 @@ +/* eslint-disable max-lines-per-function */ +"use client"; + + +/** + * Deploying page explaining how to build and assemble documentation sites for deployment, + * serving each project under a predictable URL prefix. + * + * @return The rendered Deploying page. + */ +const Deploying = () => { + return ( +
+
+

Deploying this site

+

+ A deployment of YScope docs includes both the current site and the sites of + several other YScope repos and their release versions, complicating the + build and deployment processes. The end goal is to use a single node + process to serve each site at a different URL prefix (e.g., CLP at + {" "} + /clp/main + {" "} + and clp-ffi-py at + {" "} + /clp-ffi-py/main + {""} + ). We also want the layout on disk to be predictable so that we can use a + simple config file to configure all the sites that need to be served. +

+

+ One approach would be to build all the sites using Task and then assemble + them into a single deployment. However, since each site (and version) + might have different build dependencies, we can't easily build them + without several containers. Instead, we plan to have a GitHub workflow per + repo which builds the site in a container and then publishes it directly to + docs.yscope.com at the expected location. +

+

+ Until those workflows are ready, we will deploy by building all the sites + individually and assembling them into a single deployment. +

+ +

Deployment structure

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectOn-disk pathURL prefix
yscope-docsbuild/html/
clp @ mainbuild/clp-main/build/docs/html/clp/main
clp-ffi-py @ mainbuild/clp-ffi-py-main/build/docs/html/clp-ffi-py/main
clp-ffi-py @ v0.0.9build/clp-ffi-py-v0.0.9/build/docs/html/clp-ffi-py/v0.0.9
+

+ This assumes the deployment is in + {" "} + build + . Essentially, for each + project besides yscope-docs, the build output should be in + {" "} + build/<project>-<version>/build/docs/html + {""} + . +

+ +
+

Step-by-step guide

+
    +
  1. + Build a clean version of this docs site: +
    +  task clean && task docs:build +
  2. +
  3. + Download the projects: +
    +  task docs:download-projects +
  4. +
  5. + Build the docs for each project and version in + {" "} + task docs:download-projects + {""} + : +
    + +  cd build/<project>-<version> +
    +  task docs:site +
    +  cd ../ +
    +
  6. +
  7. + Assemble a release: +
    +  task docs:release-tar +
    + The tar will be written to + {" "} + build/yscope-docs-release.tar.gz + {""} + . +
  8. +
  9. + Upload the tar to the deployment server and untar it. +
  10. +
  11. + Follow the instructions in + {" "} + yscope-docs-release/server/README.md + {" "} + to start the server. +
  12. +
+
+
+ ); +}; + +export default Deploying; diff --git a/docs/app/dev-guide/page.tsx b/docs/app/dev-guide/page.tsx new file mode 100644 index 0000000..f77c5a6 --- /dev/null +++ b/docs/app/dev-guide/page.tsx @@ -0,0 +1,45 @@ +"use client"; + +/** + * Developer Guide index page component. + * + * @return The rendered Developer Guide index section. + */ +const DevGuideIndex = () => { + return ( +
+
+

Developer Guide

+

+ This section contains docs describing our development practices and the + like. +

+ +

Contribution guidelines

+ + +

Misc

+ +
+
+ ); +}; + +export default DevGuideIndex; diff --git a/docs/app/favicon.ico b/docs/app/favicon.ico new file mode 100644 index 0000000..6bf891f Binary files /dev/null and b/docs/app/favicon.ico differ diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx new file mode 100644 index 0000000..5b7e1e6 --- /dev/null +++ b/docs/app/layout.tsx @@ -0,0 +1,84 @@ +import {GoogleAnalytics} from "@next/third-parties/google"; +import type { + Metadata, + Viewport, +} from "next"; +import { + Geist, + Geist_Mono, +} from "next/font/google"; +import Script from "next/script"; + +import Footer from "../components/sections/Footer"; +import Navbar from "../components/sections/Navbar"; + +import "./assets/scss/styles.scss"; + + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + + +export const metadata: Metadata = { + title: "YScope — CLP Documentation & Guides", + description: "Comprehensive documentation for CLP and YScope's other projects: installation, " + + "configuration, usage, and API references for users and developers.", +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, +}; + +import {ThemeProvider} from "./shared/ThemeProvider"; + + +/** + * Renders root layout. + * + * @param root0 + * @param root0.children + * @return root layout for the site. + */ +const RootLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( + + + + + + + + +