diff --git a/internal/dev_server/ui/dist/index.html b/internal/dev_server/ui/dist/index.html index f97092cf..d71785b0 100644 --- a/internal/dev_server/ui/dist/index.html +++ b/internal/dev_server/ui/dist/index.html @@ -5,7 +5,7 @@ LaunchDevly - - diff --git a/internal/dev_server/ui/src/App.css b/internal/dev_server/ui/src/App.css index 7bd331de..586121ca 100644 --- a/internal/dev_server/ui/src/App.css +++ b/internal/dev_server/ui/src/App.css @@ -2,6 +2,23 @@ @import url('../node_modules/@launchpad-ui/tokens/dist/media-queries.css'); @import url('../node_modules/@launchpad-ui/tokens/dist/themes.css'); +@font-face { + font-family: inter; + font-style: normal; + font-weight: 300 800; + font-display: swap; + src: url('https://fonts.gstatic.com/s/inter/v7/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Audimat 3000 Regulier'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Audimat3000-Regulier.var-subset.woff2') format('woff2'); +} + html, body, #root { @@ -14,6 +31,19 @@ body, padding: 2rem; } +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Audimat 3000 Regulier', sans-serif; +} + +span { + font-family: 'Inter', sans-serif; +} + .container { max-width: 40rem; margin: 0 auto; diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index ce1ba773..1657ca05 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -1,323 +1,86 @@ -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, Alert, CopyToClipboard } from '@launchpad-ui/core'; +import SyncButton from './Sync.tsx'; +import { LDFlagSet } from 'launchdarkly-js-client-sdk'; +import { Heading, Text } from '@launchpad-ui/components'; function App() { + const [selectedProject, setSelectedProject] = useState(null); 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 [showBanner, setShowBanner] = useState(false); return ( - <> -
+
+
- - + {showBanner && ( + + + No projects. + Add one via + + ldcli dev-server add-project --help + + + + )} + {!showBanner && ( + + + + + )} + {selectedProject && ( + + + + )} -
    - {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); + }} + > +