diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/.gitignore b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/.gitignore new file mode 100644 index 0000000..6dde684 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/.gitignore @@ -0,0 +1,28 @@ +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment variables +.env +.env.local +.env.*.local diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/AGENTS.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/AGENTS.md new file mode 100644 index 0000000..561e006 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/AGENTS.md @@ -0,0 +1,44 @@ +# sarah-s-app-tue-aug-18-3-34-53-pm + +This is a Datadog App scaffolded with `npm create @datadog/apps`. + +Datadog Apps are React + TypeScript applications bundled by Vite and embedded in the Datadog product experience. Frontend code runs in the browser. Backend functions live in `*.backend.*` files and execute through Datadog's managed runtime. + +## DRUIDS components + +This app uses the published `@datadog/druids` package. In scaffolded apps, import components from `@datadog/druids/[category]/[Component]`; do not import private web-ui paths like `@druids/ui/...`. + +- The scaffold already wraps the app in `DruidsEnvironment` and imports `@datadog/druids/styles.css` at the React root. Keep that setup in place if you restructure the root. +- Discover available components by browsing the installed package. Treat the `exports` map in `node_modules/@datadog/druids/package.json` as the source of truth for public entry points, and explore the corresponding category directories under `node_modules/@datadog/druids/dist` instead of relying on a hard-coded component list. +- After choosing an exported component, inspect its TypeScript definitions under `node_modules/@datadog/druids/dist/**/*.d.mts` before inventing props or imports. For icons, only use entry points that are present in the installed package. +- Start with the [DRUIDS docs](https://druids.datadoghq.com/), especially [Foundations](https://druids.datadoghq.com/foundations), [Spacing & Layout](https://druids.datadoghq.com/foundations/spacing-and-layout), [Typography](https://druids.datadoghq.com/foundations/typography), and [Components](https://druids.datadoghq.com/components). The docs cover the full internal DRUIDS library; the public `@datadog/druids` package exposes only a subset for Datadog Apps. +- Use the docs after confirming the local API to understand design intent, layout guidance, examples, and component selection tradeoffs. +- Prefer appropriate DRUIDS layout and typography components over ad hoc HTML wrappers or inline CSS. Use component props and design tokens for spacing, size, and variants. + +## Read relevant guides + +- For embedded Datadog app context, routing, navigation, browser storage, or parent-page constraints, read `docs/agents/runtime-context.md`. +- For writing backend functions in `*.backend.ts` or `*.backend.js` files, or for calling backend functions from frontend code, read `docs/agents/backend-functions.md`. +- For local development, auth, deploy, publish, `DD_APPS_PUBLISH`, or deploy troubleshooting, read `docs/agents/build-upload-auth.md`. +- For GitHub Actions CI/CD setup, read `docs/agents/cicd.md`. +- For choosing between DDSQL and Action Catalog, configuring Connections, or querying app datastores, read `docs/agents/data.md`. +- For triggering or polling a Workflow Automation workflow from a backend function, read `docs/agents/workflow-automation.md`. +- For upgrading `@datadog/vite-plugin` or `@datadog/action-catalog`, read `docs/agents/upgrading.md`. + +## General rules + +- Keep privileged Datadog API calls, third-party calls, and secret-dependent work in backend functions. +- Do not hardcode API keys, app keys, OAuth tokens, passwords, or third-party credentials. +- Set the `apps.name` and `apps.description` fields in the `datadogVitePlugin` configuration in `vite.config.ts` to concise, user-facing values that describe the app. +- Prefer the generated npm scripts in `package.json`; this scaffold uses npm. +- Never change `apps.identifier` in `vite.config.ts`. It is this app's permanent identity, not the app's UUID from App Builder — changing it makes the next upload create a duplicate app. +- Before inventing package imports or component props, inspect installed package exports and TypeScript definitions. + +## Broader Datadog Apps guidance + +- For scaffolding a new app from scratch, use the Datadog Apps agent skill when it is available. +- Use Playwright when available for browser validation, including screenshots for visual design changes. +- Use the Datadog `pup` CLI (https://github.com/DataDog/pup) when available for Datadog API inspection and troubleshooting. +- Use Datadog MCP tools when available for Datadog-specific lookup, diagnostics, or API-backed workflows. + +Primary docs: https://docs.datadoghq.com/actions/datadog_apps/ diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/CLAUDE.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/backend-functions.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/backend-functions.md new file mode 100644 index 0000000..cf5f067 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/backend-functions.md @@ -0,0 +1,91 @@ +# Backend Functions + +Backend functions are Datadog-managed server-side functions. They do not ship to the browser. + +## File and export conventions + +- Backend files match `*.backend.ts`, `*.backend.js`, `*.backend.tsx`, or `*.backend.jsx`. +- Export named async functions from backend files. +- Frontend code imports backend functions like normal ES modules. +- The Datadog Vite plugin rewrites backend imports into proxy calls at build time. + +## Runtime behavior + +- Local development bundles backend functions through Vite middleware and executes them through Datadog preview infrastructure. +- Deployed apps call backend functions through the embedded Datadog runtime bridge. +- Backend functions do not run in the user's local Node.js process. + +## Backend helper library + +Datadog provides `@datadog/apps-backend`, a library of tools to make backend function development easier. Check it out when working on backend functions. + +## Security rules + +- Keep privileged Datadog API calls, third-party calls, and secret-dependent work in backend functions. +- Never hardcode API keys, app keys, OAuth tokens, passwords, private keys, or third-party credentials. +- Frontend code should not call Datadog APIs or other privileged services directly. +- Validate frontend inputs before using them in backend function calls. + +## Action Catalog + +Prefer `@datadog/action-catalog` for supported Datadog, cloud, SaaS, and HTTP workflows. Check the installed package exports before generating imports; do not invent subpaths. + +```ts +// src/listHosts.backend.ts +import { listHosts, type ListHostsResponse } from '@datadog/action-catalog/dd/hosts'; + +export async function getHosts(filter?: string): Promise { + return listHosts({ + inputs: { + filter: filter ?? '*', + count: 10, + include_hosts_metadata: true, + }, + }); +} +``` + +```tsx +// src/App.tsx +import { useQuery } from '@tanstack/react-query'; + +import { getHosts } from './listHosts.backend'; + +function HostCount() { + const hostsQuery = useQuery({ + queryKey: ['hosts', '*'], + queryFn: () => getHosts('*'), + }); + + if (hostsQuery.isLoading) { + return Loading hosts...; + } + + if (hostsQuery.isError) { + return Unable to load hosts.; + } + + return {hostsQuery.data?.host_list?.length ?? 0}; +} +``` + +Always wrap backend function proxies before passing them to React Query or +another framework callback. Do not use `queryFn: getHosts`: React Query calls +its callback with a context object containing an `AbortSignal`, and the +embedded app bridge cannot structured-clone that object for a backend +invocation. Pass only the cloneable arguments the backend function declares: + +```tsx +// Correct +queryFn: () => getHosts('*'), + +// Incorrect +queryFn: getHosts, +``` + +The generated app wraps React in `QueryClientProvider` in `src/main.tsx`. Prefer React Query for backend function calls that need loading, error, caching, retry, refetch, or deduplication behavior. + +## Broader data workflows + +- For choosing between DDSQL and Action Catalog, configuring Connections, or querying app datastores, read `docs/agents/data.md`. +- For triggering or polling a Workflow Automation workflow, read `docs/agents/workflow-automation.md`. diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/build-upload-auth.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/build-upload-auth.md new file mode 100644 index 0000000..9b125b3 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/build-upload-auth.md @@ -0,0 +1,156 @@ +# Build, Auth, Deploy, And Publish + +This scaffold uses npm. Prefer the scripts in `package.json` over ad hoc commands. + +## Scripts + +- `npm run dev` — start the local Vite dev server. +- `npm run typecheck` — TypeScript check without emitting files. Use this for credential-free validation. +- `npm run lint` / `npm run lint:fix` — run ESLint. +- `npm run build` — production build (does not deploy by default). +- `npm run deploy` — build, upload, and publish the app live. +- `npm run publish` — publish the most recently uploaded version without rebuilding. + +## App identity (do not change) + +- `apps.identifier` in `vite.config.ts` is this app's permanent identity. It was generated once when the project was scaffolded. +- Every upload updates the app that matches this identifier. **Never change it.** Changing it — including pasting the app's UUID from App Builder into it — makes the next upload create a brand-new duplicate app instead of updating the existing one. +- `apps.identifier` is NOT the app UUID shown in App Builder. The UUID is assigned by Datadog; the identifier is a stable local key. Do not copy one into the other. + +## Auth + +Apps use **OAuth by default**. On first `npm run dev`, a browser window opens for Datadog login and the token is cached in the system keyring. No credentials needed in advance. + +**API key fallback** — use when OAuth isn't available (headless CI, restricted networks): + +1. Confirm `.env.local` is gitignored — the scaffold includes `*.local` in `.gitignore` by default. +2. Create `.env.local` at the project root with placeholders: + +``` +DD_API_KEY=REPLACE_WITH_YOUR_API_KEY +DD_APP_KEY=REPLACE_WITH_YOUR_APP_KEY +``` + +3. Open the file for editing — do not ask users to paste key values into the conversation: + +```bash +cursor .env.local 2>/dev/null || code .env.local 2>/dev/null || open .env.local +``` + +4. Direct the user to replace the placeholders. Keys are at: + - API keys: `https://app.datadoghq.com/organization-settings/api-keys` + - Application keys: `https://app.datadoghq.com/organization-settings/application-keys` — the key needs **two scopes**: **Actions API Access** and **Apps** + +Vite reads `.env.local` automatically once real values are in place. + +**`.env.local` not being picked up** — the scaffolded `vite.config.ts` reads credentials via `process.env`, which does not include `.env.local` at config evaluation time. Fix with Vite's `loadEnv`: + +```ts +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + return { + plugins: [datadogVitePlugin({ auth: { apiKey: env.DD_API_KEY, appKey: env.DD_APP_KEY } })], + }; +}); +``` + +Restart the dev server after updating `vite.config.ts`. + +## Non-US1 site + +Set `auth.site` in `vite.config.ts` so local dev and deploys target the right site: + +```ts +datadogVitePlugin({ auth: { site: '' } }); +``` + +## Deploy + +Build, upload, and publish in one command: + +```bash +npm run deploy +``` + +With API keys (CI environments): + +```bash +export DD_API_KEY="" +export DD_APP_KEY="" +npm run deploy +``` + +A successful deploy prints a Datadog URL for the app. + +**Deploy without publishing** — upload as a draft without going live: + +```bash +npm run deploy -- --no-publish +# or +DD_APPS_PUBLISH=false npm run deploy +``` + +`DD_APPS_PUBLISH=false` is useful in CI when publishing is a separate step. + +## Publish + +Publish the most recently uploaded version without rebuilding: + +```bash +npm run publish +``` + +Publish a specific version by ID: + +```bash +npm run publish -- --version +``` + +When using `--version` without a cache file, set `DD_APPS_IDENTIFIER` to the app's identifier. + +**Older scaffolded projects** — if `deploy` and `publish` scripts are missing, add them to `package.json` (requires `@datadog/vite-plugin` >= 3.2.0): + +```json +{ "scripts": { "deploy": "datadog-apps deploy", "publish": "datadog-apps publish" } } +``` + +## Troubleshooting + +**401 / missing authentication token / credentials not configured** + +- OAuth: if the browser window didn't open, the environment may not support it. Fall back to API keys. +- API keys: verify `DD_API_KEY` and `DD_APP_KEY` are set and the application key has both **Actions API Access** and **Apps** scopes. +- Check `auth.site` in `vite.config.ts` matches the credentials' Datadog site. + +**403 "you do not have access to this app" on upload** + +The application key needs both scopes enabled — not just one: + +1. **Actions API Access** — required for backend function execution. +2. **Apps** (or **App Builder**) — required for uploading and publishing. + +Go to `https://app.datadoghq.com/organization-settings/application-keys`, confirm both scopes, or create a new key with both. + +**Build succeeds but nothing deploys** + +- Use `npm run deploy`, not `npm run build`, when the intent is to deploy. +- Check whether `DD_APPS_PUBLISH` is set to `false` in the environment — the app uploads as a draft but does not go live. Unset the variable or run `npm run publish` separately. +- Confirm `dryRun` in `vite.config.ts` is not `true`. +- Confirm `DD_APPS_UPLOAD_ASSETS` is set — `npm run deploy` does this automatically. + +**Build fails with missing credentials** + +- Current scaffold versions may make `npm run build` exercise Datadog deploy behavior. +- Cache OAuth credentials first (`npm run dev`), or set `DD_API_KEY` and `DD_APP_KEY`. +- For credential-free validation, prefer `npm run typecheck`. + +**Node or scaffolding errors** + +- Check `package.json` `engines.node` for the supported Node.js versions. +- Use Volta, nvm, or fnm to switch versions. Prefer a current Node 22 release when debugging. + +**Datadog site mismatch** + +- Inspect `vite.config.ts` for the configured site and confirm CI uses the same value. diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/cicd.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/cicd.md new file mode 100644 index 0000000..8928185 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/cicd.md @@ -0,0 +1,42 @@ +# CI/CD + +## GitHub Actions + +Use `DataDog/apps-github-action` to deploy on pushes to the deployment branch. Keep `app-directory` aligned with the app's path in the repository. + +```yaml +name: Continuous Deployment +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + deploy-app: + name: Deploy Datadog App + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + + - name: Deploy + uses: DataDog/apps-github-action@v0.0.2 + with: + datadog-api-key: ${{ secrets.DATADOG_API_KEY }} + datadog-app-key: ${{ secrets.DATADOG_APP_KEY }} + app-directory: . +``` + +Store `DATADOG_API_KEY` and `DATADOG_APP_KEY` as [encrypted secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) in the repository or organization settings. The application key needs both **Actions API Access** and **Apps** scopes. + +For non-US1 organizations, configure the Datadog site in `vite.config.ts` and ensure CI and local config are aligned. diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/data.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/data.md new file mode 100644 index 0000000..9be21be --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/data.md @@ -0,0 +1,61 @@ +# Data Access + +Backend functions have two primary data access paths: DDSQL and Action Catalog. + +## Choosing a path + +| Situation | Use | +| --- | --- | +| Read from Datadog-visible data (logs, metrics, hosts, spans, etc.) and need filtering, projection, joins, aggregation, or pagination | DDSQL | +| Mutation — create, update, delete, trigger, invoke, send | Action Catalog | +| Product workflow that maps to an existing typed action | Action Catalog | +| Simple single-record read where the typed action response is exactly what the UI needs | Action Catalog | +| External system or integration that is not DDSQL-visible | Action Catalog | +| Read from an app datastore | DDSQL (prefer for queries) or datastore actions (for simple reads/writes) | + +## DDSQL + +DDSQL-visible datasets include logs, APM spans, RUM events, hosts, containers, services, cloud inventory, audit trail, CI pipelines, and more. Check the [DDSQL Data Directory](https://docs.datadoghq.com/ddsql_reference/data_directory/) for the full list. + +Before writing queries: + +- Confirm the table name, columns, and types with a small bounded probe before writing app code. +- Verify the exact DDSQL execution API or action import against installed package exports — do not invent subpaths. +- Use fixed SQL templates. Do not accept raw frontend SQL or raw `ORDER BY` fields. +- Clamp limits and use `LIMIT` in all list queries. + +### App datastores + +Datastores are queryable via DDSQL. Confirm the datastore ID and column schema with a bounded probe first: + +```sql +SELECT key, summary, status +FROM dd.actions_datastores( + id => '', + columns => ARRAY ['key', 'summary', 'status'] +) AS ( + key VARCHAR, + summary VARCHAR, + status VARCHAR +) +WHERE status = 'Open' +ORDER BY key +LIMIT 100; +``` + +For writes and deletes, use datastore actions from `@datadog/action-catalog` directly. + +Public docs: [Datastores](https://docs.datadoghq.com/actions/datastores/), [DDSQL Reference](https://docs.datadoghq.com/ddsql_reference/) + +## Connections + +Connections are reusable auth configuration for Action Catalog actions that need credentials (custom HTTP, integrations without a Datadog tile, etc.). + +- Find or create connections at `https://app.datadoghq.com/actions/connections`. +- Copy the Connection ID from the connection details — treat it as configuration, not a secret. +- Do not ask users to paste API keys, passwords, OAuth secrets, or private keys into an AI conversation. +- Have a human create or update connections in the Datadog UI. + +Integrations such as GitHub, Jira, Slack, PagerDuty, and Statuspage may inherit credentials from their Datadog integration tile and may not need an explicit connection. + +Public docs: [Connections](https://docs.datadoghq.com/actions/connections/), [Action Catalog](https://docs.datadoghq.com/actions/action_interface/) diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/runtime-context.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/runtime-context.md new file mode 100644 index 0000000..6d30e71 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/runtime-context.md @@ -0,0 +1,30 @@ +# Runtime Context + +Datadog Apps are embedded inside the Datadog UI. Treat this app as iframe-hosted browser code with Datadog-managed backend execution. + +## Iframe embedding + +- The app runs inside an iframe owned by the Datadog UI. +- The current App Builder iframe sandbox allows scripts, popups, popups escaping the sandbox, same-origin access, forms, and top navigation only by user activation. +- Do not assume app code can control, read, or directly navigate the parent Datadog page. +- Prefer hash-based routing for in-app routes. Avoid `BrowserRouter` and routers that depend on reliable top-level History API behavior. +- In-app links should use hash URLs such as `href="#/details"`. +- External URLs can open popups or top-level navigation only through browser-supported, user-initiated flows. +- Verify the current Datadog embedding sandbox before relying on a specific browser capability. Datadog controls the sandbox attributes and may change them. + +## Storage and cookies + +- Browser storage is scoped to the app's origin and is not shared with the parent Datadog UI. +- Storage can differ between standalone local development and embedded runtime because modern browsers partition storage by top-level site and iframe origin. +- Third-party cookies are unreliable for durable app state. +- For durable server-side state, use backend functions and a Datadog-supported durable storage mechanism. + +## Backend call transport + +Frontend code imports backend functions like normal ES modules, but those imports are rewritten by the Datadog Vite plugin. The function body does not run in the browser. + +- In local dev, backend calls go through Vite middleware and Datadog preview infrastructure. +- In deployed apps, backend calls go through the Datadog iframe/runtime bridge. +- Do not build app logic that depends on backend functions running in the local Node.js process. + +For backend implementation details, read `backend-functions.md`. diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/upgrading.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/upgrading.md new file mode 100644 index 0000000..19b5c4e --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/upgrading.md @@ -0,0 +1,63 @@ +# Upgrading + +## Primary packages + +- `@datadog/vite-plugin`: Vite integration for local development, build, and deploy. +- `@datadog/action-catalog`: typed Action Catalog client for backend functions. +- `@datadog/apps-backend`: Datadog-provided helpers for backend function development. + +Preserve the existing package manager and lockfile style. + +## Before upgrading + +Check release notes before changing versions: + +```bash +npm view @datadog/vite-plugin version repository.url homepage +npm view @datadog/action-catalog version repository.url homepage +npm view @datadog/apps-backend version repository.url homepage +``` + +Inspect the current app: + +```bash +npm outdated @datadog/vite-plugin @datadog/action-catalog @datadog/apps-backend +npm ls @datadog/vite-plugin @datadog/action-catalog @datadog/apps-backend +``` + +## Upgrade + +```bash +npm install @datadog/action-catalog@latest +npm install @datadog/apps-backend@latest +npm install -D @datadog/vite-plugin@latest +``` + +After upgrading, verify: + +- `vite.config.ts` Datadog plugin config, especially `auth.site`. +- `package.json` scripts — check for new `deploy` and `publish` scripts added in `@datadog/vite-plugin` 3.2.0+. +- Backend function imports from `@datadog/action-catalog/...`. +- Project-local instructions in `AGENTS.md`. + +## Compare against a fresh scaffold + +To pull in changes from the latest scaffolder without guessing: + +```bash +tmp_dir="$(mktemp -d)" +npm create @datadog/apps@latest -- "$tmp_dir/base-app" --template vite-react -y --skip-post-scaffold +diff -ru "$tmp_dir/base-app/package.json" package.json +diff -ru "$tmp_dir/base-app/vite.config.ts" vite.config.ts +diff -ru "$tmp_dir/base-app/src" src +``` + +Port only the specific changes needed. Do not replace the app wholesale. + +## Verify + +```bash +npm run typecheck +npm run lint +npm run build +``` diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/workflow-automation.md b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/workflow-automation.md new file mode 100644 index 0000000..2ec9bcd --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/docs/agents/workflow-automation.md @@ -0,0 +1,150 @@ +# Workflow Automation + +Use this when a backend function needs to trigger a Datadog Workflow Automation workflow and poll for the result. + +## Requirements + +- Use a backend function, not frontend code. Backend functions can call `@datadog/action-catalog`. +- Use the generic HTTP action from `@datadog/action-catalog/http/http`. +- The HTTP action requires a Connection ID for an HTTP connection configured in your Datadog org. Find or create one at `https://app.datadoghq.com/actions/connections`. +- The target workflow must be published and must include an API trigger (`apiTrigger` in the workflow spec). +- Workflow inputs must match the workflow input schema exactly. + +## Inspect the workflow first + +Before wiring an app to a workflow, confirm its published shape: + +```bash +workflow_id="" +curl -sS --fail-with-body \ + "https://api.datadoghq.com/api/v2/workflows/${workflow_id}" \ + -H "DD-API-KEY: ${DD_API_KEY}" \ + -H "DD-APPLICATION-KEY: ${DD_APP_KEY}" \ + -H "Accept: application/json" \ +| jq "{ + id: .data.id, + name: .data.attributes.name, + published: .data.attributes.published, + inputSchema: .data.attributes.spec.inputSchema, + outputSchema: .data.attributes.spec.outputSchema, + hasApiTrigger: any(.data.attributes.spec.triggers[]?; has(\"apiTrigger\")) + }" +``` + +Confirm `published` is `true`, `hasApiTrigger` is `true`, and `inputSchema.parameters` matches the inputs the app will send. + +## Trigger and poll pattern + +Inputs go under `meta.payload`. Replace `YOUR_HTTP_CONNECTION_ID` with the ID from your Datadog org. + +```ts +import { request } from '@datadog/action-catalog/http/http'; + +const DATADOG_HTTP_CONNECTION_ID = ''; +const DATADOG_API_BASE_URL = 'https://api.datadoghq.com'; +const DEFAULT_POLL_TIMEOUT_MS = 120_000; +const INITIAL_POLL_DELAY_MS = 250; +const POLL_DELAY_MULTIPLIER = 1.05; + +const jsonHeaders = [ + { key: 'Accept', value: ['application/json'] }, + { key: 'Content-Type', value: ['application/json'] }, +]; + +type WorkflowInstanceResponse = { + data?: { + id?: string; + attributes?: { + endTimestamp?: string | null; + outputs?: unknown; + instanceStatus?: { detailsKind?: string; displayName?: string }; + }; + }; +}; + +type WorkflowRunResult = { + instanceId?: string; + statusKind?: string; + displayStatus?: string; + outputs?: unknown; + body: unknown; +}; + +function workflowInstancesUrl(workflowId: string) { + return `${DATADOG_API_BASE_URL}/api/v2/workflows/${workflowId}/instances`; +} + +function isRunningStatus(statusKind?: string) { + return !statusKind || statusKind === 'IN_PROGRESS'; +} + +function toRunResult(body: unknown): WorkflowRunResult { + const response = body as WorkflowInstanceResponse | undefined; + return { + instanceId: response?.data?.id, + statusKind: response?.data?.attributes?.instanceStatus?.detailsKind, + displayStatus: response?.data?.attributes?.instanceStatus?.displayName, + outputs: response?.data?.attributes?.outputs, + body, + }; +} + +export async function triggerWorkflow( + workflowId: string, + payload: Record, +): Promise { + const response = await request({ + connectionId: DATADOG_HTTP_CONNECTION_ID, + inputs: { + verb: 'POST', + url: workflowInstancesUrl(workflowId), + requestHeaders: jsonHeaders, + responseParsing: 'json', + errorOnStatus: ['400-599'], + body: JSON.stringify({ meta: { payload } }), + }, + }); + return toRunResult(response.body); +} + +export async function pollWorkflowInstance( + workflowId: string, + instanceId: string, + timeoutMs = DEFAULT_POLL_TIMEOUT_MS, +): Promise { + const expiresAt = Date.now() + timeoutMs; + let delay = INITIAL_POLL_DELAY_MS / POLL_DELAY_MULTIPLIER; + + while (Date.now() < expiresAt) { + delay *= POLL_DELAY_MULTIPLIER; + const jitter = delay * 0.4 * (Math.random() - 0.5); + await new Promise((resolve) => setTimeout(resolve, delay + jitter)); + + const response = await request({ + connectionId: DATADOG_HTTP_CONNECTION_ID, + inputs: { + verb: 'GET', + url: `${workflowInstancesUrl(workflowId)}/${instanceId}`, + requestHeaders: jsonHeaders, + responseParsing: 'json', + errorOnStatus: ['400-599'], + }, + }); + + const result = toRunResult(response.body); + if (!isRunningStatus(result.statusKind)) { + return result; + } + } + + throw new Error(`Workflow instance ${instanceId} did not finish in time`); +} +``` + +Treat `statusKind === "SUCCEEDED"` as success. All other terminal statuses (`INSTANCE_ERROR`, `STEP_ERROR`, `CANCELED`) are failures — return the full response body so the caller can inspect `errorDetail` and partial outputs. + +## Troubleshooting + +- `input parameter not in workflow input schema`: update `meta.payload` keys to match `inputSchema.parameters`. +- `Expected trigger type TRIGGER_TYPE_API not found`: add and publish an API trigger on the workflow. +- 401/403: confirm `DD_API_KEY` and `DD_APP_KEY` are set with Actions API Access, and the app site matches the API host. diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/eslint.config.mjs b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/eslint.config.mjs new file mode 100644 index 0000000..83eea8d --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/eslint.config.mjs @@ -0,0 +1,23 @@ +import js from '@eslint/js'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + ...tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/index.html b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/index.html new file mode 100644 index 0000000..e914e68 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/index.html @@ -0,0 +1,15 @@ + + + + + + + sarah-s-app-tue-aug-18-3-34-53-pm + + + +
+ + + + diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/package.json b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/package.json new file mode 100644 index 0000000..50dc4f9 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/package.json @@ -0,0 +1,43 @@ +{ + "name": "sarah-s-app-tue-aug-18-3-34-53-pm", + "version": "0.0.1", + "private": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "upload": "DD_APPS_UPLOAD_ASSETS=1 vite build", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --fix", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@datadog/action-catalog": "^0.0.3", + "@datadog/apps-backend": "^0.0.1", + "@datadog/druids": "^0.0.7", + "@tanstack/react-query": "^5.101.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "optionalDependencies": { + "@napi-rs/keyring": "^1.3.0" + }, + "devDependencies": { + "@datadog/vite-plugin": "^3.2.1", + "@eslint/js": "^9.39.2", + "@types/node": "^24.12.2", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "eslint": "^9.39.2", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^16.5.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1" + } +} \ No newline at end of file diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/public/favicon.ico b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/public/favicon.ico new file mode 100644 index 0000000..e04da3e Binary files /dev/null and b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/public/favicon.ico differ diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.css b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.css new file mode 100644 index 0000000..d203bda --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.css @@ -0,0 +1,143 @@ +.app-shell { + align-items: center; + background: + linear-gradient(135deg, rgba(99, 44, 166, 0.12), transparent 34%), + linear-gradient(315deg, rgba(0, 149, 161, 0.12), transparent 40%), + #f7f8fb; + box-sizing: border-box; + color: #1f2933; + display: flex; + min-height: 100vh; + justify-content: center; + padding: 32px; +} + +.calculator { + background: #ffffff; + border: 1px solid #d9dee8; + border-radius: 8px; + box-shadow: 0 24px 70px rgba(30, 39, 60, 0.16); + box-sizing: border-box; + display: grid; + gap: 18px; + max-width: 380px; + padding: 22px; + width: min(100%, 380px); +} + +.calculator-header { + display: grid; + gap: 4px; +} + +.eyebrow { + color: #6b7280; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + line-height: 1.2; + margin: 0; + text-transform: uppercase; +} + +.calculator h1 { + color: #111827; + font-size: 28px; + line-height: 1.15; + margin: 0; +} + +.display { + align-items: center; + background: #121826; + border-radius: 8px; + color: #f8fafc; + display: flex; + font-variant-numeric: tabular-nums; + font-size: clamp(30px, 9vw, 46px); + font-weight: 700; + justify-content: flex-end; + min-height: 92px; + overflow: hidden; + padding: 16px 18px; + text-align: right; + white-space: nowrap; +} + +.keypad { + display: grid; + gap: 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.key { + align-items: center; + appearance: none; + background: #eef2f7; + border: 1px solid #d7dde8; + border-radius: 8px; + color: #1f2937; + cursor: pointer; + display: flex; + font: inherit; + font-size: 20px; + font-weight: 700; + justify-content: center; + min-height: 58px; + padding: 0 8px; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease, + transform 120ms ease; +} + +.key:hover { + background: #e1e7f0; +} + +.key:active { + transform: translateY(1px); +} + +.key:focus-visible { + outline: 3px solid rgba(99, 44, 166, 0.32); + outline-offset: 2px; +} + +.key-operator { + background: #632ca6; + border-color: #632ca6; + color: #ffffff; +} + +.key-operator:hover, +.key-active { + background: #4f2384; + border-color: #4f2384; +} + +.key-danger { + background: #fff0f0; + border-color: #ffc9c9; + color: #b42318; +} + +.key-danger:hover { + background: #ffe2e2; +} + +@media (max-width: 420px) { + .app-shell { + padding: 16px; + } + + .calculator { + padding: 16px; + } + + .key { + font-size: 18px; + min-height: 52px; + } +} diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.tsx b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.tsx new file mode 100644 index 0000000..7cb97a7 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/App.tsx @@ -0,0 +1,296 @@ +import { useMemo, useState } from 'react'; + +import './App.css'; + +type Operator = 'add' | 'subtract' | 'multiply' | 'divide'; + +type CalculatorState = { + display: string; + storedValue: number | null; + pendingOperator: Operator | null; + shouldResetDisplay: boolean; +}; + +type CalculatorKey = + | { kind: 'clear'; label: string } + | { kind: 'sign'; label: string } + | { kind: 'percent'; label: string } + | { kind: 'backspace'; label: string; ariaLabel: string } + | { kind: 'digit'; label: string; value: string } + | { kind: 'decimal'; label: string } + | { kind: 'operator'; label: string; value: Operator } + | { kind: 'equals'; label: string }; + +const initialState: CalculatorState = { + display: '0', + storedValue: null, + pendingOperator: null, + shouldResetDisplay: false, +}; + +const keys: CalculatorKey[] = [ + { kind: 'clear', label: 'C' }, + { kind: 'sign', label: '+/-' }, + { kind: 'percent', label: '%' }, + { kind: 'operator', label: '/', value: 'divide' }, + { kind: 'digit', label: '7', value: '7' }, + { kind: 'digit', label: '8', value: '8' }, + { kind: 'digit', label: '9', value: '9' }, + { kind: 'operator', label: 'x', value: 'multiply' }, + { kind: 'digit', label: '4', value: '4' }, + { kind: 'digit', label: '5', value: '5' }, + { kind: 'digit', label: '6', value: '6' }, + { kind: 'operator', label: '-', value: 'subtract' }, + { kind: 'digit', label: '1', value: '1' }, + { kind: 'digit', label: '2', value: '2' }, + { kind: 'digit', label: '3', value: '3' }, + { kind: 'operator', label: '+', value: 'add' }, + { kind: 'backspace', label: 'Del', ariaLabel: 'Delete last digit' }, + { kind: 'digit', label: '0', value: '0' }, + { kind: 'decimal', label: '.' }, + { kind: 'equals', label: '=' }, +]; + +function calculate(left: number, right: number, operator: Operator): number | null { + switch (operator) { + case 'add': + return left + right; + case 'subtract': + return left - right; + case 'multiply': + return left * right; + case 'divide': + return right === 0 ? null : left / right; + } +} + +function formatDisplay(value: number): string { + if (!Number.isFinite(value)) { + return 'Error'; + } + + const rounded = Number(value.toPrecision(12)); + return String(rounded); +} + +function inputDigit(state: CalculatorState, digit: string): CalculatorState { + if (state.display === 'Error' || state.shouldResetDisplay) { + return { + ...state, + display: digit, + shouldResetDisplay: false, + }; + } + + return { + ...state, + display: state.display === '0' ? digit : `${state.display}${digit}`, + }; +} + +function inputDecimal(state: CalculatorState): CalculatorState { + if (state.display === 'Error' || state.shouldResetDisplay) { + return { + ...state, + display: '0.', + shouldResetDisplay: false, + }; + } + + if (state.display.includes('.')) { + return state; + } + + return { + ...state, + display: `${state.display}.`, + }; +} + +function setOperator(state: CalculatorState, operator: Operator): CalculatorState { + if (state.display === 'Error') { + return initialState; + } + + const currentValue = Number(state.display); + + if (state.storedValue === null || state.pendingOperator === null) { + return { + display: state.display, + storedValue: currentValue, + pendingOperator: operator, + shouldResetDisplay: true, + }; + } + + if (state.shouldResetDisplay) { + return { + ...state, + pendingOperator: operator, + }; + } + + const result = calculate(state.storedValue, currentValue, state.pendingOperator); + if (result === null) { + return { + ...initialState, + display: 'Error', + shouldResetDisplay: true, + }; + } + + return { + display: formatDisplay(result), + storedValue: result, + pendingOperator: operator, + shouldResetDisplay: true, + }; +} + +function finishCalculation(state: CalculatorState): CalculatorState { + if ( + state.display === 'Error' || + state.storedValue === null || + state.pendingOperator === null + ) { + return state; + } + + const result = calculate( + state.storedValue, + Number(state.display), + state.pendingOperator, + ); + + if (result === null) { + return { + ...initialState, + display: 'Error', + shouldResetDisplay: true, + }; + } + + return { + display: formatDisplay(result), + storedValue: null, + pendingOperator: null, + shouldResetDisplay: true, + }; +} + +function applyKey(state: CalculatorState, key: CalculatorKey): CalculatorState { + switch (key.kind) { + case 'clear': + return initialState; + case 'sign': + if (state.display === '0' || state.display === 'Error') { + return state; + } + return { + ...state, + display: state.display.startsWith('-') + ? state.display.slice(1) + : `-${state.display}`, + }; + case 'percent': + if (state.display === 'Error') { + return state; + } + return { + ...state, + display: formatDisplay(Number(state.display) / 100), + }; + case 'backspace': + if (state.display === 'Error' || state.shouldResetDisplay) { + return { + ...state, + display: '0', + shouldResetDisplay: false, + }; + } + return { + ...state, + display: + state.display.length > 1 + ? state.display.slice(0, -1) + : '0', + }; + case 'digit': + return inputDigit(state, key.value); + case 'decimal': + return inputDecimal(state); + case 'operator': + return setOperator(state, key.value); + case 'equals': + return finishCalculation(state); + } +} + +function App() { + const [state, setState] = useState(initialState); + const activeOperator = state.pendingOperator; + + const displayValue = useMemo(() => { + if (state.display === 'Error' || state.display.endsWith('.')) { + return state.display; + } + + return state.display.includes('.') + ? state.display + : new Intl.NumberFormat('en-US', { + maximumFractionDigits: 10, + useGrouping: true, + }).format(Number(state.display)); + }, [state.display]); + + return ( +
+
+
+

Datadog App

+

Calculator

+
+ + {displayValue} + +
+ {keys.map((key) => { + const isOperator = + key.kind === 'operator' || key.kind === 'equals'; + const isActiveOperator = + key.kind === 'operator' && + key.value === activeOperator && + state.shouldResetDisplay; + + return ( + + ); + })} +
+
+
+ ); +} + +export default App; diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/main.tsx b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/main.tsx new file mode 100644 index 0000000..8f2c295 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/src/main.tsx @@ -0,0 +1,24 @@ +import '@datadog/druids/styles.css'; + +import { DruidsEnvironment } from '@datadog/druids/layout/DruidsEnvironment'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import App from './App'; + +const queryClient = new QueryClient(); +const appRoot = document.getElementById('app'); +if (appRoot) { + createRoot(appRoot).render( + + + + + + + , + ); +} else { + console.error('Missing #app div for createRoot.'); +} diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/tsconfig.json b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/tsconfig.json new file mode 100644 index 0000000..7a01668 --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/apps/sarah-s-app-tue-aug-18-3-34-53-pm/vite.config.ts b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/vite.config.ts new file mode 100644 index 0000000..7d6ab7a --- /dev/null +++ b/apps/sarah-s-app-tue-aug-18-3-34-53-pm/vite.config.ts @@ -0,0 +1,59 @@ +import { datadogVitePlugin } from '@datadog/vite-plugin'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +import { version } from './package.json'; +import rootManifest from '../../package.json'; + +process.env.DD_SITE ||= rootManifest.datadogApps?.site; +process.env.DATADOG_SITE ||= process.env.DD_SITE; + +const hasDatadogApiKeys = Boolean( + (process.env.DD_API_KEY || process.env.DATADOG_API_KEY) && + (process.env.DD_APP_KEY || process.env.DATADOG_APP_KEY), +); + +export default defineConfig({ + base: './', + build: { + sourcemap: true, + }, + plugins: [ + react(), + datadogVitePlugin({ + logLevel: 'debug', + auth: { + site: process.env.DD_SITE, + apiKey: process.env.DD_API_KEY, + appKey: process.env.DD_APP_KEY, + }, + apps: { + enable: true, + name: 'Calculator', + description: 'A compact calculator for quick arithmetic.', + authOverrides: { + method: hasDatadogApiKeys ? 'apiKey' : 'oauth', + }, + // Stable identity for this app, generated once when the project was scaffolded. + // DO NOT change this value, and DO NOT set it to the app's UUID from App Builder. + // Every upload targets the app matching this identifier — changing it makes the + // next upload create a brand-new app instead of updating this one. + identifier: '5ef349a1b04ee57966e08f519394f24a', + }, + errorTracking: { + enable: hasDatadogApiKeys, + sourcemaps: { + minifiedPathPrefix: '/', + releaseVersion: version, + service: 'sarah-s-app-tue-aug-18-3-34-53-pm', + } + }, + metadata: { + name: 'Calculator', + }, + metrics: { + enable: hasDatadogApiKeys, + }, + }), + ], +}); diff --git a/package-lock.json b/package-lock.json index f322b2c..4034f5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,104 @@ "node": "^20.19.0 || >=22.12.0" } }, + "apps/sarah-s-app-tue-aug-18-3-34-53-pm": { + "version": "0.0.1", + "dependencies": { + "@datadog/action-catalog": "^0.0.3", + "@datadog/apps-backend": "^0.0.1", + "@datadog/druids": "^0.0.7", + "@tanstack/react-query": "^5.101.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@datadog/vite-plugin": "^3.2.1", + "@eslint/js": "^9.39.2", + "@types/node": "^24.12.2", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "eslint": "^9.39.2", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^16.5.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.3.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@napi-rs/keyring": "^1.3.0" + } + }, + "apps/sarah-s-app-tue-aug-18-3-34-53-pm/node_modules/@datadog/vite-plugin": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@datadog/vite-plugin/-/vite-plugin-3.2.12.tgz", + "integrity": "sha512-W9wQyjxCOEQW1Smq5nj7KeObL7ie+kHwWg/ifrCFEGXMJliRlkBCqY1E54ms2qYfssE4+F5H4egmY7BkOSB+6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", + "async-retry": "1.3.3", + "chalk": "2.3.1", + "eslint-scope": "7.2.2", + "glob": "11.1.0", + "json-stream-stringify": "3.1.6", + "jszip": "3.10.1", + "magic-string": "0.30.21", + "oauth4webapi": "3.8.6", + "outdent": "0.8.0", + "p-queue": "6.6.2", + "pretty-bytes": "5.6.0", + "simple-git": "3.36.0", + "unplugin": "2.3.11" + }, + "optionalDependencies": { + "@napi-rs/keyring": "1.3.0" + }, + "peerDependencies": { + "@babel/parser": "^7.24.5", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "vite": ">= 5.x <= 8.x" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@babel/traverse": { + "optional": true + }, + "@babel/types": { + "optional": true + } + } + }, + "apps/sarah-s-app-tue-aug-18-3-34-53-pm/node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "apps/sarah-s-app-tue-aug-18-3-34-53-pm/node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, "apps/sarah-s-app-wed-aug-12-1-34-27-pm": { "version": "0.0.1", "dependencies": { @@ -358,6 +456,35 @@ "node": ">=6.9.0" } }, + "node_modules/@datadog/action-catalog": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@datadog/action-catalog/-/action-catalog-0.0.3.tgz", + "integrity": "sha512-gRi29i6SYbICcWBPH2fW+6iifvjQ22kVmJf4n/PCM4c1hDd0OUekmnrgyGtLb+3YNspUzM/3vBgArbXJfHnSMw==", + "license": "Apache-2.0" + }, + "node_modules/@datadog/apps-backend": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@datadog/apps-backend/-/apps-backend-0.0.1.tgz", + "integrity": "sha512-utOkUOd0hb3q58wWDUbm+GAvYGI8fv3bNpikUj723HAZtAO4wd47c/2uWOANIBsnhQqA9QOnomqex+LmBxhDTA==", + "license": "Apache-2.0" + }, + "node_modules/@datadog/druids": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@datadog/druids/-/druids-0.0.7.tgz", + "integrity": "sha512-pzmDIgwWXsuNLa6Kr4TlbHLkx8szvmLNCWYHQSWSdRKDhP8AZgGyk4XI/HwXy7aONThOHuaCTI94c+AP6l1CwA==", + "license": "SEE LICENSE IN EULA.txt", + "dependencies": { + "@popperjs/core": "2.11.8", + "@tippyjs/react": "4.2.6", + "react-popper": "2.3.0", + "react-table": "7.8.0", + "tippy.js": "6.3.7" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@datadog/js-instrumentation-wasm": { "version": "1.0.8", "dev": true, @@ -1372,6 +1499,17 @@ "node": ">= 10" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "dev": true, @@ -1738,6 +1876,45 @@ "@simple-git/args-pathspec": "^1.0.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -1789,7 +1966,6 @@ "version": "24.13.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2914,7 +3090,6 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3041,6 +3216,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -3340,6 +3527,63 @@ "node": ">=6" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "dev": true, @@ -3348,6 +3592,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-table": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", + "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "dev": true, @@ -3426,6 +3683,10 @@ "dev": true, "license": "MIT" }, + "node_modules/sarah-s-app-tue-aug-18-3-34-53-pm": { + "resolved": "apps/sarah-s-app-tue-aug-18-3-34-53-pm", + "link": true + }, "node_modules/sarah-s-app-wed-aug-12-1-34-27-pm": { "resolved": "apps/sarah-s-app-wed-aug-12-1-34-27-pm", "link": true @@ -3551,6 +3812,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "dev": true, @@ -3586,6 +3856,264 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/undici-types": { "version": "7.18.2", "dev": true, @@ -3721,6 +4249,15 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "dev": true,