diff --git a/.env b/.env index 4a7e37cccc..adc21721c8 100644 --- a/.env +++ b/.env @@ -129,6 +129,11 @@ SHIPPING_SERVICE_DOCKERFILE=./src/shippingservice/Dockerfile FLAGD_HOST=flagd FLAGD_PORT=8013 +#flagd-ui +FLAGD_UI_HOST=flagd-ui +FLAGD_UI_PORT=4000 +FLAGD_UI_DOCKERFILE=./src/flagd-ui/Dockerfile + # Kafka KAFKA_SERVICE_PORT=9092 KAFKA_SERVICE_ADDR=kafka:${KAFKA_SERVICE_PORT} diff --git a/.github/workflows/component-build-images.yml b/.github/workflows/component-build-images.yml index 1b76ddf611..d209aeb29b 100644 --- a/.github/workflows/component-build-images.yml +++ b/.github/workflows/component-build-images.yml @@ -111,6 +111,10 @@ jobs: tag_suffix: shippingservice context: ./ setup-qemu: true + - file: ./src/flagd-ui/Dockerfile + tag_suffix: flagdui + context: ./ + setup-qemu: true - file: ./test/tracetesting/Dockerfile tag_suffix: traceBasedTests context: ./ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9905856c53..9c64bfae2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ the release. ([#1727](https://github.com/open-telemetry/opentelemetry-demo/pull/1727)) * [chore] Fix binding for host's volume mount ([#1728](https://github.com/open-telemetry/opentelemetry-demo/pull/1728)) +* [flagd-ui] Add UI for managing Flagd feature flags + ([#1725](https://github.com/open-telemetry/opentelemetry-demo/pull/1725)) ## 1.11.1 @@ -687,4 +689,4 @@ significant modifications will be credited to OpenTelemetry Authors. * Added span attributes to frontend service ([#82](https://github.com/open-telemetry/opentelemetry-demo/pull/82)) * Rewrote shipping service in Rust -([#35](https://github.com/open-telemetry/opentelemetry-demo/issues/35)) +([#35](https://github.com/open-telemetry/opentelemetry-demo/issues/35)) \ No newline at end of file diff --git a/Makefile b/Makefile index 2156a458c8..f368a0ed10 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ start: @echo "Go to http://localhost:8080/jaeger/ui for the Jaeger UI." @echo "Go to http://localhost:8080/grafana/ for the Grafana UI." @echo "Go to http://localhost:8080/loadgen/ for the Load Generator UI." - @echo "Go to https://opentelemetry.io/docs/demo/feature-flags/ to learn how to change feature flags." + @echo "Go to http://localhost:8080/feature/ to to change feature flags." .PHONY: start-minimal start-minimal: diff --git a/docker-compose.yml b/docker-compose.yml index 6369c57432..151c535ae6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -340,6 +340,8 @@ services: - ENVOY_PORT - FLAGD_HOST - FLAGD_PORT + - FLAGD_UI_HOST + - FLAGD_UI_PORT depends_on: frontend: condition: service_started @@ -349,6 +351,8 @@ services: condition: service_started grafana: condition: service_started + flagd-ui: + condition: service_started # Imageprovider imageprovider: @@ -596,6 +600,33 @@ services: - ./src/flagd:/etc/flagd logging: *logging + + #Flagd-ui, UI for configuring the feature flagging service + flagd-ui: + image: ${IMAGE_NAME}:${DEMO_VERSION}-flagd-ui + container_name: flagd-ui + build: + context: ./ + dockerfile: ${FLAGD_UI_DOCKERFILE} + deploy: + resources: + limits: + memory: 150M + restart: unless-stopped + environment: + - OTEL_EXPORTER_OTLP_ENDPOINT=http://${OTEL_COLLECTOR_HOST}:${OTEL_COLLECTOR_PORT_HTTP} + - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE + - OTEL_RESOURCE_ATTRIBUTES + - OTEL_SERVICE_NAME=flagd-ui + ports: + - "${FLAGD_UI_PORT}" + depends_on: + otelcol: + condition: service_started + flagd: + condition: service_started + volumes: + - ./src/flagd:/app/data # Kafka used by Checkout, Accounting, and Fraud Detection services kafka: @@ -768,7 +799,7 @@ services: ports: - "9200" healthcheck: - test: curl -s http://localhost:9200/_cluster/health | grep '"status":"green"' + test: curl -s http://localhost:9200/_cluster/health | grep -q '"status":"green"' start_period: 10s interval: 5s timeout: 10s diff --git a/src/flagd-ui/.dockerignore b/src/flagd-ui/.dockerignore new file mode 100644 index 0000000000..b68454fa99 --- /dev/null +++ b/src/flagd-ui/.dockerignore @@ -0,0 +1,56 @@ +# Dependency directories +node_modules +/.pnp +.pnp.js + +# Next.js build output +.next +out + +# Testing +/coverage + +# Production +/build + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env*.local + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# IDE/Editor folders +.idea +.vscode + +# OS generated files +Thumbs.db + +# Temporary files +*.swp +*.swo + +# Git related +.git +.gitignore + +# Docker related +Dockerfile +.dockerignore + +# Other +README.md +*.log diff --git a/src/flagd-ui/.prettierrc b/src/flagd-ui/.prettierrc new file mode 100644 index 0000000000..b4bfed3579 --- /dev/null +++ b/src/flagd-ui/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/src/flagd-ui/Dockerfile b/src/flagd-ui/Dockerfile index e326aa2d62..6bc1b4c6d7 100644 --- a/src/flagd-ui/Dockerfile +++ b/src/flagd-ui/Dockerfile @@ -1,6 +1,33 @@ -FROM node:20.15.1-slim -WORKDIR /data -COPY . . -RUN npm install -CMD [ "/bin/bash -c 'npm run dev'" ] +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +FROM node:20 AS builder + +WORKDIR /app + +COPY ./src/flagd-ui/package*.json ./ + +RUN npm ci + +COPY ./src/flagd-ui/. ./ + +RUN npm run build + +# ----------------------------------------------------------------------------- + +FROM node:20-alpine + +WORKDIR /app + +COPY ./src/flagd-ui/package*.json ./ + +RUN npm ci --only=production + +COPY --from=builder /app/src/instrumentation.ts ./instrumentation.ts +COPY --from=builder /app/next.config.mjs ./next.config.mjs + +COPY --from=builder /app/.next ./.next + +EXPOSE 4000 + +CMD ["npm", "start"] diff --git a/src/flagd-ui/README.md b/src/flagd-ui/README.md index c4033664f8..4a1e499087 100644 --- a/src/flagd-ui/README.md +++ b/src/flagd-ui/README.md @@ -1,36 +1,21 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +# Flagd-ui -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +This application provides a user interface for configuring the feature flags of the flagd service. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -To learn more about Next.js, take a look at the following resources: +## Running the application -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +The application can be run with the rest of the demo using the documented docker compose or make commands. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +## Local development -## Deploy on Vercel +To run the app locally for development you must copy ```src/flagd/demo.flagd.json``` into ```src/flagd-ui/data/demo.flagd.json``` +(create the directory and file if they do not exist yet). Make sure you're in the ```src/flagd-ui``` directory and run +the following command: -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +```bash +npm run dev +``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Then you must navigate to ```localhost:4000/feature```. \ No newline at end of file diff --git a/src/flagd-ui/data/output.json b/src/flagd-ui/data/output.json deleted file mode 100644 index e15129f3fe..0000000000 --- a/src/flagd-ui/data/output.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "https://flagd.dev/schema/v0/flags.json", - "flags": { - "productCatalogFailure": { - "description": "Fail product catalog service on a specific product", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "recommendationServiceCacheFailure": { - "description": "Fail recommendation service cache", - "state": "DISABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - }, - "adServiceManualGc": { - "description": "Triggers full manual garbage collections in the ad service", - "state": "DISABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "on" - }, - "adServiceHighCpu": { - "description": "Triggers high cpu load in the ad service", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "adServiceFailure": { - "description": "Fail ad service", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "kafkaQueueProblems": { - "description": "Overloads Kafka queue while simultaneously introducing a consumer side delay leading to a lag spike", - "state": "ENABLED", - "variants": { - "on": 100, - "off": 0 - }, - "defaultVariant": "off" - }, - "cartServiceFailure": { - "description": "Fail cart service", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "paymentServiceFailure": { - "description": "Fail payment service charge requests", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "paymentServiceUnreachable": { - "description": "Payment service is unavailable", - "state": "ENABLED", - "variants": { - "on": true, - "off": false - }, - "defaultVariant": "off" - }, - "loadgeneratorFloodHomepage": { - "description": "Flood the frontend with a large amount of requests.", - "state": "ENABLED", - "variants": { - "on": 100, - "off": 0 - }, - "defaultVariant": "off" - }, - "imageSlowLoad": { - "description": "slow loading images in the frontend", - "state": "ENABLED", - "variants": { - "10sec": 10000, - "5sec": 5000, - "off": 0 - }, - "defaultVariant": "off" - } - } -} \ No newline at end of file diff --git a/src/flagd-ui/next.config.mjs b/src/flagd-ui/next.config.mjs index 4678774e6d..85f1012825 100644 --- a/src/flagd-ui/next.config.mjs +++ b/src/flagd-ui/next.config.mjs @@ -1,4 +1,9 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + experimental: { + instrumentationHook: true, + }, + basePath: "/feature", +}; export default nextConfig; diff --git a/src/flagd-ui/package-lock.json b/src/flagd-ui/package-lock.json index 157e5576ea..0e519853c4 100644 --- a/src/flagd-ui/package-lock.json +++ b/src/flagd-ui/package-lock.json @@ -8,19 +8,24 @@ "name": "flagd-ui", "version": "0.1.0", "dependencies": { + "@vercel/otel": "^1.10.0", "ajv": "^8.17.1", "next": "14.2.5", "react": "^18", "react-dom": "^18" }, "devDependencies": { + "@types/json5": "^2.2.0", "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.20", "eslint": "^8", "eslint-config-next": "14.2.5", - "postcss": "^8", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.41", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.10", "typescript": "^5" } }, @@ -425,6 +430,146 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -456,10 +601,15 @@ } }, "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==", + "deprecated": "This is a stub types definition. json5 provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "*" + } }, "node_modules/@types/node": { "version": "20.14.15", @@ -477,10 +627,11 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -491,10 +642,18 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT", + "peer": true + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -628,11 +787,28 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vercel/otel": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@vercel/otel/-/otel-1.10.0.tgz", + "integrity": "sha512-bv1FXbFZlFbB89vyA2P9/kr6eZ42bMtXgqBJpgi+8yOrZU8rkg9wMi0TL//AgAf/qKtaryDm1sbCnkLHgCI3PQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0", + "@opentelemetry/api-logs": ">=0.46.0 && <1.0.0", + "@opentelemetry/instrumentation": ">=0.46.0 && <1.0.0", + "@opentelemetry/resources": "^1.19.0", + "@opentelemetry/sdk-logs": ">=0.46.0 && <1.0.0", + "@opentelemetry/sdk-metrics": "^1.19.0", + "@opentelemetry/sdk-trace-base": "^1.19.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -640,6 +816,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -893,6 +1079,44 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -966,6 +1190,39 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1085,6 +1342,13 @@ "node": ">= 6" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz", + "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==", + "license": "MIT", + "peer": true + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1216,7 +1480,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1343,6 +1606,13 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/electron-to-chromium": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1540,6 +1810,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2130,6 +2410,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2154,7 +2448,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2439,7 +2732,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2472,6 +2764,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2615,7 +2920,6 @@ "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", - "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -3156,11 +3460,17 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "license": "MIT", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -3272,6 +3582,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3281,6 +3598,16 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3515,8 +3842,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -3606,6 +3932,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -3751,6 +4078,101 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz", + "integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3888,11 +4310,25 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz", + "integrity": "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4040,7 +4476,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -4101,6 +4536,13 @@ "node": ">=8" } }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -4434,7 +4876,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4447,6 +4888,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4557,6 +4999,13 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -4693,6 +5142,37 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/src/flagd-ui/package.json b/src/flagd-ui/package.json index 77855a5194..769ccd3f5e 100644 --- a/src/flagd-ui/package.json +++ b/src/flagd-ui/package.json @@ -3,25 +3,30 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 4000 -H 0.0.0.0", "build": "next build", - "start": "next start", + "start": "next start -p 4000 -H 0.0.0.0", "lint": "next lint" }, "dependencies": { + "@vercel/otel": "^1.10.0", "ajv": "^8.17.1", "next": "14.2.5", "react": "^18", "react-dom": "^18" }, "devDependencies": { + "@types/json5": "^2.2.0", "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.20", "eslint": "^8", "eslint-config-next": "14.2.5", - "postcss": "^8", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.41", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.10", "typescript": "^5" } } diff --git a/src/flagd-ui/pages/advanced.tsx b/src/flagd-ui/pages/advanced.tsx deleted file mode 100644 index 9cccf571db..0000000000 --- a/src/flagd-ui/pages/advanced.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import FileEditor from "../src/app/components/FileEditor"; - -export default function Home() { - const [flagData, setflagData] = useState(null); - const [reloadData, setreloadData] = useState(false); - useEffect(() => { - const readFile = (file_name: string) => { - const fileContent = fetch(`/api/read-file?${file_name}`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }) - .then((response) => response.json()) - .then((data) => { - setflagData(data); - console.log(data); - }) - .catch((error) => console.error(error)); - }; - - // Initial read - readFile(""); - if (reloadData) { - setreloadData(false); - } - }, [reloadData]); - return ( -
- - - - -
- ); -} diff --git a/src/flagd-ui/pages/api/read-file.ts b/src/flagd-ui/pages/api/read-file.ts deleted file mode 100644 index 68504885d9..0000000000 --- a/src/flagd-ui/pages/api/read-file.ts +++ /dev/null @@ -1,31 +0,0 @@ -import fs from "fs"; -import path from "path"; -// import data_file from "../../../flagd/demo.flagd.json"; -import type { NextApiResponse, NextApiRequest } from "next"; -import { isUtf8 } from "buffer"; - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const file_name = req.query.file_name?.toString() || "output.json"; - if (req.method === "GET") { - const data = JSON.parse( - fs.readFileSync(path.join(process.cwd(), "data", file_name), { - encoding: "utf8", - flag: "r", - }) - ); - res.status(200).json(data); - } else { - res.status(405).json({ message: "Method not allowed" }); - } -} -export function readFileContents(file_name: string) { - fetch(`/api/read-file?${file_name}`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - }) - .then((response) => response.json()) - .then((data) => data); -} diff --git a/src/flagd-ui/pages/api/write-to-file.ts b/src/flagd-ui/pages/api/write-to-file.ts deleted file mode 100644 index c872acb21b..0000000000 --- a/src/flagd-ui/pages/api/write-to-file.ts +++ /dev/null @@ -1,23 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { isUtf8 } from "buffer"; -import type { NextApiResponse, NextApiRequest } from "next"; - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === "POST") { - const { data } = req.body; - const filePath = path.join(process.cwd(), "data", "output.json"); - fs.writeFile(filePath, JSON.stringify(data, null, 2), (err) => { - if (err) { - res.status(500).json({ message: "Error writing to file" }); - return; - } - res.status(200).json({ message: "File written successfully" }); - }); - } else { - res.status(405).json({ message: "Method not allowed" }); - } -} diff --git a/src/flagd-ui/public/next.svg b/src/flagd-ui/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/src/flagd-ui/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/flagd-ui/public/vercel.svg b/src/flagd-ui/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/src/flagd-ui/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/flagd-ui/src/app/advanced/page.tsx b/src/flagd-ui/src/app/advanced/page.tsx new file mode 100644 index 0000000000..7815103d88 --- /dev/null +++ b/src/flagd-ui/src/app/advanced/page.tsx @@ -0,0 +1,9 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import AdvancedView from "@/components/advanced/AdvancedView"; + +const Advanced = () => { + return ; +}; + +export default Advanced; diff --git a/src/flagd-ui/src/app/api/read-file/route.ts b/src/flagd-ui/src/app/api/read-file/route.ts new file mode 100644 index 0000000000..67d598d2b4 --- /dev/null +++ b/src/flagd-ui/src/app/api/read-file/route.ts @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const file_name = searchParams.get("file_name") || "demo.flagd.json"; + + try { + const filePath = path.join(process.cwd(), "data", file_name); + const fileContents = fs.readFileSync(filePath, "utf8"); + const data = JSON.parse(fileContents); + return NextResponse.json(data); + } catch (error) { + console.error("Error reading file:", error); + return NextResponse.json({ error: "Failed to read file" }, { status: 500 }); + } +} diff --git a/src/flagd-ui/src/app/api/write-to-file/route.ts b/src/flagd-ui/src/app/api/write-to-file/route.ts new file mode 100644 index 0000000000..b90bad9383 --- /dev/null +++ b/src/flagd-ui/src/app/api/write-to-file/route.ts @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { NextResponse } from "next/server"; +import fs from "fs/promises"; +import path from "path"; + +export async function POST(request: Request) { + try { + const { data } = await request.json(); + const filePath = path.join(process.cwd(), "data", "demo.flagd.json"); + + await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf8"); + + return NextResponse.json( + { message: "File written successfully" }, + { status: 200 }, + ); + } catch (error) { + console.error("Error writing to file:", error); + return NextResponse.json( + { message: "Error writing to file" }, + { status: 500 }, + ); + } +} diff --git a/src/flagd-ui/src/app/components/FeatureFlag.css b/src/flagd-ui/src/app/components/FeatureFlag.css deleted file mode 100644 index 4a9eedf9e7..0000000000 --- a/src/flagd-ui/src/app/components/FeatureFlag.css +++ /dev/null @@ -1,43 +0,0 @@ -/* .feature-flag { - background-color: red; -} */ -.feature-flag { - display: flex; - align-items: center; /* centers items vertically */ - justify-content: space-between; /* spaces items horizontally */ - padding: 20px; - /* margin: auto; - width: 400px; - height: 100px; - cursor: pointer; */ - border: 2px solid #007bff; /* Blue border */ -} -.send-button { - margin-left: auto; -} -.top-right-name { - position: relative; - top: 0; - right: 0; -} -.toggle-button { - /* padding: 2px; */ - /* text-indent: 50px; */ -} -.name-div { - width: 500px; -} -/* .right-center-toggle-box{ - margin: auto; -} */ -.input_not_matched_case { - border: 2px solid red; -} -/* .enable-disable-checkbox{ - padding: 20px; -} */ -.my-checkbox { - padding: 2px; - /* text-align-last: 50px; */ - /* text-indent: 50px; */ -} diff --git a/src/flagd-ui/src/app/components/FeatureFlag.tsx b/src/flagd-ui/src/app/components/FeatureFlag.tsx deleted file mode 100644 index 43de6872dd..0000000000 --- a/src/flagd-ui/src/app/components/FeatureFlag.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; -import "./FeatureFlag.css"; -import React, { useCallback } from "react"; -import { useState } from "react"; - -type FeatureFlagsProps = { - flagId: string; - setreloadData: (param: boolean) => void; - flagConfig: FlagConfig; - configFile?: ConfigFile; -}; - -function FeatureFlag({ - flagId, - setreloadData, - flagConfig, - configFile, -}: FeatureFlagsProps) { - const [enableDisable, setEnableDisable] = useState( - flagConfig.state === "ENABLED" - ); - const [toggleOn, setToggleOn] = useState(flagConfig.defaultVariant === "on"); // TODO adopt this - const enableDisableMapping = { - on: true, - off: false, - }; - - const handleEnableDisableChange = useCallback( - (e: { - target: { checked: boolean | ((prevState: boolean) => boolean) }; - }) => { - setEnableDisable(e.target.checked); - }, - [] - ); - - const handleToggleOnClick = useCallback(() => { - setToggleOn(!toggleOn); - }, [toggleOn]); - - function sendUpdate() { - configFile.flags[flagId].state = enableDisable ? "ENABLED" : "DISABLED"; - configFile.flags[flagId].defaultVariant = toggleOn ? "on" : "off"; // TODO adopt this - fetch("/api/write-to-file", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ data: configFile }), - }) - .then((response) => response.json()) - .then((data) => console.log(data)) - .then(() => setreloadData(true)) - .catch((error) => console.error(error)); - return; - } - const DynamicField = ({ flagConfig, flagId, setreloadData }) => { - // Object.values(flagConfig.variants).every(value => typeof value === 'boolean') - switch (JSON.stringify(flagConfig.variants)) { - case JSON.stringify(enableDisableMapping): - return ( -
- -
- ); - default: - return
; - } - }; - return ( -
-
-
{flagId}
- - -
- - -
- ); -} - -export default FeatureFlag; \ No newline at end of file diff --git a/src/flagd-ui/src/app/components/FileEditor.tsx b/src/flagd-ui/src/app/components/FileEditor.tsx deleted file mode 100644 index a21c70f9d6..0000000000 --- a/src/flagd-ui/src/app/components/FileEditor.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; -import Ajv from "ajv"; - -import React, { useCallback, useEffect, useRef } from "react"; -import { useState } from "react"; - -async function makeRequest(url: string) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error: any) { - console.error("There was an error:", error.message); - return null; - } -} -const ajv = new Ajv(); -let validate: any; - -// @ts-ignore -function FileEditor({ setreloadData, flagConfig }) { - const [schema, setSchema] = useState<[any, any][]>([]); - useEffect(() => { - async function loadSchema() { - const schema1 = await makeRequest( - "https://flagd.dev/schema/v0/flags.json" - ); - const schema2 = await makeRequest( - "https://flagd.dev/schema/v0/targeting.json" - ); // if there are more in the future - const schemas: [any, any][] = [schema1, schema2]; - setSchema(schemas); - validate = ajv.addSchema(schemas[1]).compile(schemas[0]); - } - loadSchema(); - }, []); - const textAreaRef = useRef(null); - function attemptUpdate() { - function parseJSON() { - try { - // @ts-ignore - const data = JSON.parse(textAreaRef.current.value); - return data; - } catch (objError) { - window.alert("Error parsing JSON"); - if (objError instanceof SyntaxError) { - console.error(objError.name); - } else { - console.error(objError); - } - return null; - } - } - const data = parseJSON(); - if (data === null) return; - validate(data) ? sendUpdate(data) : window.alert("Schema check failed"); - } - - function sendUpdate(data: any) { - fetch("/api/write-to-file", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ data }), - }) - .then((response) => response.json()) - .then((data) => console.log(data)) - .then(() => setreloadData(true)) - .catch((error) => console.error(error)); - return; - } - // @ts-ignore - const DynamicField = ({ flagConfig }) => { - return ( - - ); - }; - return ( -
- - -
- ); -} - -export default FileEditor; diff --git a/src/flagd-ui/src/app/globals.css b/src/flagd-ui/src/app/globals.css index 875c01e819..b5c61c9567 100644 --- a/src/flagd-ui/src/app/globals.css +++ b/src/flagd-ui/src/app/globals.css @@ -1,33 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/src/flagd-ui/src/app/layout.tsx b/src/flagd-ui/src/app/layout.tsx index 5ff842ef07..12d26901f6 100644 --- a/src/flagd-ui/src/app/layout.tsx +++ b/src/flagd-ui/src/app/layout.tsx @@ -1,6 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { Layout } from "@/components/Layout"; +import "./globals.css"; + export const metadata = { - title: "Next.js", - description: "Generated by Next.js", + title: "Flagd Configurator", + description: + "Built to provide an easier way to configure the Flagd configurations", }; export default function RootLayout({ @@ -10,7 +16,9 @@ export default function RootLayout({ }) { return ( - {children} + + {children} + ); } diff --git a/src/flagd-ui/src/app/page.tsx b/src/flagd-ui/src/app/page.tsx index c9601d3874..db43fd7b53 100644 --- a/src/flagd-ui/src/app/page.tsx +++ b/src/flagd-ui/src/app/page.tsx @@ -1,52 +1,13 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 "use client"; -import { useEffect, useState } from "react"; -import FeatureFlag from "./components/FeatureFlag"; -import Link from "next/link"; +import React from "react"; +import BasicView from "../components/basic/BasicView"; export default function Home() { - const [flagData, setflagData] = useState(null); - const [reloadData, setreloadData] = useState(false); - useEffect(() => { - const readFile = () => { - const fileContent = fetch("/api/read-file", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }) - .then((response) => response.json()) - .then((data) => { - setflagData(data); - console.log(data); - }) - .catch((error) => console.error(error)); - }; - readFile(); - if (reloadData) { - setreloadData(false); - } - }, [reloadData]); - return (
- - - - {flagData && - Object.keys(flagData.flags).map((flagId) => { - const flagConfig: FlagConfig = flagData.flags[flagId]; - return ( - - ); - })} +
); } diff --git a/src/flagd-ui/src/components/Layout.tsx b/src/flagd-ui/src/components/Layout.tsx new file mode 100644 index 0000000000..dbe8097086 --- /dev/null +++ b/src/flagd-ui/src/components/Layout.tsx @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +"use client"; +import React, { useState, createContext, useContext } from "react"; +import NavBar from "./nav/NavBar"; +import Spinner from "./utils/Spinner"; + +type LoadingContextType = { + isLoading: boolean; + setIsLoading: React.Dispatch>; +}; + +const LoadingContext = createContext(undefined); + +export const useLoading = () => { + const context = useContext(LoadingContext); + if (context === undefined) { + throw new Error("useLoading must be used within a LoadingProvider"); + } + return context; +}; + +export const Layout: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [isLoading, setIsLoading] = useState(false); + + return ( + +
+ +
{children}
+ {isLoading && } +
+
+ ); +}; diff --git a/src/flagd-ui/src/components/advanced/AdvancedView.tsx b/src/flagd-ui/src/components/advanced/AdvancedView.tsx new file mode 100644 index 0000000000..9ed1ecdf18 --- /dev/null +++ b/src/flagd-ui/src/components/advanced/AdvancedView.tsx @@ -0,0 +1,160 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +"use client"; +import { useEffect, useRef, useState } from "react"; +import FileEditor from "./FileEditor"; +import Ajv, { AnySchema } from "ajv"; +import { useLoading } from "../Layout"; + +const ajv = new Ajv(); +let validate: any; + +export default function AdvancedView() { + const [flagData, setflagData] = useState(null); + const [reloadData, setReloadData] = useState(false); + const [flagDataIsSynced, setFlagDataIsSynced] = useState(true); + + const textAreaRef = useRef(null); + const { setIsLoading } = useLoading(); + + useEffect(() => { + const readFile = async (file_name: string) => { + try { + const response = await fetch(`/feature/api/read-file?${file_name}`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const data = await response.json(); + setflagData(data); + } catch (err: unknown) { + window.alert(err); + console.error(err); + } + }; + readFile(""); + if (reloadData) { + setReloadData(false); + } + }, [reloadData]); + + useEffect(() => { + async function loadSchema() { + try { + const schemas: [AnySchema | null, AnySchema | null] = await Promise.all( + [ + requestSchemas("https://flagd.dev/schema/v0/flags.json"), + requestSchemas("https://flagd.dev/schema/v0/targeting.json"), + ], + ); + if (schemas[0] && schemas[1]) { + validate = ajv.addSchema(schemas[1]).compile(schemas[0]); + } + } catch (error) { + console.error("Error loading schemas:", error); + } + + return null; + } + loadSchema(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function requestSchemas(url: string): Promise { + try { + setIsLoading(true); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setIsLoading(false); + return data; + } catch (error: any) { + console.error("There was an error:", error.message); + } + return null; + } + + function parseJSON(): string | null { + try { + if (textAreaRef.current) { + const data = JSON.parse(textAreaRef.current.value); + return data; + } + } catch (objError) { + window.alert("Error parsing JSON"); + if (objError instanceof SyntaxError) { + console.error(objError.name); + } else { + console.error(objError); + } + } + return null; + } + + async function saveUpdate(flagData: string): Promise { + try { + setIsLoading(true); + const response = await fetch("/feature/api/write-to-file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data: flagData }), + }); + await response.json(); + setIsLoading(false); + setReloadData(true); + setFlagDataIsSynced(true); + } catch (err: unknown) { + setIsLoading(false); + window.alert(err); + console.error(err); + } + } + + function update() { + const data = parseJSON(); + if (data === null) return; + validate(data) ? saveUpdate(data) : window.alert("Schema check failed"); + } + + const handleTextAreaChange = () => { + try { + if (textAreaRef.current) { + const textAreaContent = JSON.parse(textAreaRef.current.value); + setFlagDataIsSynced( + JSON.stringify(textAreaContent) === JSON.stringify(flagData), + ); + } + } catch (error) { + console.error("Invalid JSON in textarea", error); + setFlagDataIsSynced(false); + } + }; + + return ( + <> + {flagData && ( +
+ +
+
+ + {!flagDataIsSynced && ( +

Unsaved changes

+ )} +
+
+
+ )} + + ); +} diff --git a/src/flagd-ui/src/components/advanced/FileEditor.tsx b/src/flagd-ui/src/components/advanced/FileEditor.tsx new file mode 100644 index 0000000000..ba3c63e73f --- /dev/null +++ b/src/flagd-ui/src/components/advanced/FileEditor.tsx @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +"use client"; +import React, { RefObject } from "react"; +import { FlagConfig } from "@/utils/types"; + +type FileEditorProps = { + flagConfig: FlagConfig; + textAreaRef: RefObject; + handleTextAreaChange: () => void; +}; + +function FileEditor({ + flagConfig, + textAreaRef, + handleTextAreaChange, +}: FileEditorProps) { + return ( +