From 0406fc9b8a94d5330f9d9b030cc179d2057fddd0 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Tue, 13 Aug 2024 16:51:56 -0700 Subject: [PATCH 1/6] Add project selector and refactor components --- internal/dev_server/ui/src/App.tsx | 337 ++---------------- internal/dev_server/ui/src/Flags.tsx | 322 +++++++++++++++++ .../dev_server/ui/src/ProjectSelector.tsx | 79 ++++ internal/dev_server/ui/src/util.ts | 2 + 4 files changed, 429 insertions(+), 311 deletions(-) create mode 100644 internal/dev_server/ui/src/Flags.tsx create mode 100644 internal/dev_server/ui/src/ProjectSelector.tsx create mode 100644 internal/dev_server/ui/src/util.ts diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index ce1ba773..1f052661 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -1,322 +1,37 @@ -import { LDFlagSet, LDFlagValue } from 'launchdarkly-js-client-sdk'; -import { - Button, - Checkbox, - IconButton, - Label, - Switch, - Modal, - ModalOverlay, - DialogTrigger, - Dialog, -} from '@launchpad-ui/components'; -import { Box, InlineEdit, TextField } from '@launchpad-ui/core'; -import Theme from '@launchpad-ui/tokens'; import './App.css'; -import { useEffect, useRef, useState } from 'react'; -import { Icon } from '@launchpad-ui/icons'; - -const API_BASE = import.meta.env.PROD ? '' : '/api'; -const apiRoute = (pathname: string) => `${API_BASE}${pathname}`; +import {useState} from 'react'; +import Flags from "./Flags.tsx"; +import ProjectSelector from "./ProjectSelector.tsx"; +import {Box} from "@launchpad-ui/core"; function App() { - const [flags, setFlags] = useState(null); - const [overrides, setOverrides] = useState | null>(null); - const [onlyShowOverrides, setOnlyShowOverrides] = useState(false); - const overridesPresent = overrides && Object.keys(overrides).length > 0; - const textAreaRef = useRef(null); - - const updateOverride = (flagKey: string, overrideValue: LDFlagValue) => { - fetch(apiRoute(`/dev/projects/default/overrides/${flagKey}`), { - method: 'PUT', - body: JSON.stringify(overrideValue), - }) - .then(async (res) => { - if (!res.ok) { - return; // todo - } - - const updatedOverrides = { - ...overrides, - ...{ - [flagKey]: { - value: overrideValue, - }, - }, - }; - - setOverrides(updatedOverrides); - }) - .catch((_e) => { - // todo - }); - }; - - const removeOverride = (flagKey: string, updateState: boolean = true) => { - return fetch(apiRoute(`/dev/projects/default/overrides/${flagKey}`), { - method: 'DELETE', - }) - .then((res) => { - // todo: clean this up. - // - // In the remove-all-override case, we need to fan out and make the - // request for every override, so we don't want to be interleaving - // local state updates. Expect the consumer to update the local state - // when all requests are done - if (res.ok && updateState) { - const updatedOverrides = { ...overrides }; - delete updatedOverrides[flagKey]; - - setOverrides(updatedOverrides); - - if (Object.keys(updatedOverrides).length === 0) - setOnlyShowOverrides(false); - } - }) - .catch((_e) => { - // todo - }); - }; - - // Fetch flags / overrides on mount - useEffect(() => { - fetch(apiRoute('/dev/projects/default?expand=overrides')) - .then(async (res) => { - if (!res.ok) { - return; // todo - } - - const json = await res.json(); - const { flagsState: flags, overrides } = json; - const sortedFlags = Object.keys(flags) - .sort((a, b) => a.localeCompare(b)) - .reduce>((accum, flagKey) => { - accum[flagKey] = flags[flagKey]; - - return accum; - }, {}); - - setFlags(sortedFlags); - setOverrides(overrides); - }) - .catch((_e) => { - // todo - }); - }, []); - - if (!flags) { - return null; - } + const [selectedProject, setSelectedProject] = useState(null); return ( <> -
- - - - -
    - {Object.entries(flags).map(([flagKey, { value: flagValue }]) => { - const overrideValue = overrides?.[flagKey]?.value; - const hasOverride = overrideValue !== undefined; - let valueNode; - - if (onlyShowOverrides && !hasOverride) { - return null; - } - - switch (typeof flagValue) { - case 'boolean': - valueNode = ( - { - updateOverride(flagKey, newValue); - }} - /> - ); - break; - case 'number': - valueNode = ( - { - updateOverride(flagKey, Number(e.target.value)); - }} - /> - ); - break; - case 'string': - valueNode = ( - { - updateOverride(flagKey, newValue); - }} - renderInput={} - > - {hasOverride ? overrideValue : flagValue} - - ); - break; - default: - valueNode = ( - - - - - - {({ close }) => ( -
    { - let newVal; - - try { - newVal = JSON.parse( - textAreaRef?.current?.value || '', - ); - } catch (err) { - window.alert('Invalid JSON formatting'); - return; - } - - updateOverride(flagKey, newVal); - }} - > - + + + + + {({ close }) => ( + { + let newVal; + + try { + newVal = JSON.parse( + textAreaRef?.current?.value || '', + ); + } catch (err) { + window.alert('Invalid JSON formatting'); + return; + } + + updateOverride(flagKey, newVal); + }} + > + - - - - - {({ close }) => ( - { - let newVal; + + + + + {({ close }) => ( + { + let newVal; - try { - newVal = JSON.parse( - textAreaRef?.current?.value || '', - ); - } catch (err) { - window.alert('Invalid JSON formatting'); - return; - } + try { + newVal = JSON.parse( + textAreaRef?.current?.value || '', + ); + } catch (err) { + window.alert('Invalid JSON formatting'); + return; + } - updateOverride(flagKey, newVal); - }} - > + updateOverride(flagKey, newVal); + }} + > - - - - - {({ close }) => ( - { - let newVal; + switch (typeof flagValue) { + case 'boolean': + valueNode = ( + { + updateOverride(flagKey, newValue); + }} + /> + ); + break; + case 'number': + valueNode = ( + { + updateOverride(flagKey, Number(e.target.value)); + }} + /> + ); + break; + case 'string': + valueNode = ( + { + updateOverride(flagKey, newValue); + }} + renderInput={ + + } + > + {hasOverride ? overrideValue : flagValue} + + ); + break; + default: + valueNode = ( + + + + + + {({ close }) => ( + { + let newVal; - try { - newVal = JSON.parse( - textAreaRef?.current?.value || '', - ); - } catch (err) { - window.alert('Invalid JSON formatting'); - return; - } + try { + newVal = JSON.parse( + textAreaRef?.current?.value || '', + ); + } catch (err) { + window.alert('Invalid JSON formatting'); + return; + } - updateOverride(flagKey, newVal); - }} - > -