diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3fd1efcf88..01621cc667 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -6,8 +6,6 @@ import { useSelector } from 'react-redux'; import { createHashRouter, RouterProvider } from 'react-router-dom'; import { SWRConfig } from 'swr'; import ErrorLayout from './app/ErrorLayout'; -import EditFlag from './app/flags/EditFlag'; -import Evaluation from './app/flags/Evaluation'; import Flag from './app/flags/Flag'; import NewFlag from './app/flags/NewFlag'; import Layout from './app/Layout'; @@ -19,6 +17,8 @@ import SessionProvider from './components/SessionProvider'; import { request } from './data/api'; import { Theme } from './types/Preferences'; const Flags = loadable(() => import('./app/flags/Flags')); +const Variants = loadable(() => import('./app/flags/variants/Variants')); +const Rules = loadable(() => import('./app/flags/rules/Rules')); const Segments = loadable(() => import('./app/segments/Segments')); const Console = loadable(() => import('./app/console/Console')); const Login = loadable(() => import('./app/auth/Login')); @@ -53,11 +53,11 @@ const namespacesRoutes = [ children: [ { path: '', - element: + element: }, { - path: 'evaluation', - element: + path: 'rules', + element: } ] }, diff --git a/ui/src/app/Support.tsx b/ui/src/app/Support.tsx index a5f285bc5e..f59797f4bf 100644 --- a/ui/src/app/Support.tsx +++ b/ui/src/app/Support.tsx @@ -22,7 +22,7 @@ export default function Support() { className="text-white bg-violet-500 mb-1 inline-flex items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm hover:bg-violet-600 hover:cursor-pointer focus:outline-none focus:ring-1 focus:ring-violet-500 focus:ring-offset-1" target="_blank" rel="noreferrer" - href="https://www.flipt.io/docs/" + href="https://www.flipt.io/docs?utm_source=app" > Documentation
Send Email diff --git a/ui/src/app/flags/EditFlag.tsx b/ui/src/app/flags/EditFlag.tsx deleted file mode 100644 index ee6728d237..0000000000 --- a/ui/src/app/flags/EditFlag.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useOutletContext } from 'react-router-dom'; -import FlagForm from '~/components/flags/FlagForm'; -import MoreInfo from '~/components/MoreInfo'; -import { FlagType } from '~/types/Flag'; -import { FlagProps } from './FlagProps'; -import Rollouts from './rollouts/Rollouts'; -import Variants from './variants/Variants'; - -export default function EditFlag() { - const { flag, onFlagChange } = useOutletContext(); - - return ( - <> -
- {/* flag details */} -
-
-
-

- Basic information about the flag and its status. -

- - Learn more about flags - -
-
- -
-
-
- - {flag.type === FlagType.VARIANT && ( - - )} - {flag.type === FlagType.BOOLEAN && } -
- - ); -} diff --git a/ui/src/app/flags/Flag.tsx b/ui/src/app/flags/Flag.tsx index 3c584734bf..fb6cd14adc 100644 --- a/ui/src/app/flags/Flag.tsx +++ b/ui/src/app/flags/Flag.tsx @@ -6,23 +6,26 @@ import { import { formatDistanceToNowStrict, parseISO } from 'date-fns'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import { NavLink, Outlet, useNavigate, useParams } from 'react-router-dom'; import { selectReadonly } from '~/app/meta/metaSlice'; import { selectCurrentNamespace, selectNamespaces } from '~/app/namespaces/namespacesSlice'; +import FlagForm from '~/components/flags/FlagForm'; import Dropdown from '~/components/forms/Dropdown'; import Loading from '~/components/Loading'; import Modal from '~/components/Modal'; +import MoreInfo from '~/components/MoreInfo'; import CopyToNamespacePanel from '~/components/panels/CopyToNamespacePanel'; import DeletePanel from '~/components/panels/DeletePanel'; -import TabBar from '~/components/TabBar'; import { copyFlag, deleteFlag, getFlag } from '~/data/api'; import { useError } from '~/data/hooks/error'; import { useSuccess } from '~/data/hooks/success'; import { useTimezone } from '~/data/hooks/timezone'; import { FlagType, IFlag } from '~/types/Flag'; +import { classNames } from '~/utils/helpers'; +import Rollouts from './rollouts/Rollouts'; export default function Flag() { let { flagKey } = useParams(); @@ -47,6 +50,11 @@ export default function Flag() { setFlagVersion(flagVersion + 1); }; + const tabs = [ + { name: 'Variants', to: '' }, + { name: 'Rules', to: 'rules' } + ]; + useEffect(() => { if (!flagKey) return; @@ -62,18 +70,6 @@ export default function Flag() { if (!flag) return ; - const tabs = [ - { - name: 'Details', - to: '' - }, - { - name: 'Evaluation', - to: 'evaluation', - disabled: flag.type === FlagType.BOOLEAN - } - ]; - return ( <> {/* flag delete modal */} @@ -169,8 +165,58 @@ export default function Flag() { />
- - + +
+ {/* flag details */} +
+
+
+

+ Basic information about the flag and its status. +

+ + Learn more about flags + +
+
+ +
+
+
+ + {flag.type === FlagType.VARIANT && ( + <> +
+
+ +
+
+ + + )} + {flag.type === FlagType.BOOLEAN && } +
); } diff --git a/ui/src/app/flags/NewFlag.tsx b/ui/src/app/flags/NewFlag.tsx index 8ab867a088..7607b3fa47 100644 --- a/ui/src/app/flags/NewFlag.tsx +++ b/ui/src/app/flags/NewFlag.tsx @@ -14,9 +14,6 @@ export default function NewFlag() {
-

- Details -

Basic information about the flag and its status.

diff --git a/ui/src/app/flags/rollouts/Rollouts.tsx b/ui/src/app/flags/rollouts/Rollouts.tsx index 22a3e5a550..28fcea2edd 100644 --- a/ui/src/app/flags/rollouts/Rollouts.tsx +++ b/ui/src/app/flags/rollouts/Rollouts.tsx @@ -13,11 +13,7 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { - InformationCircleIcon, - PlusIcon, - StarIcon -} from '@heroicons/react/24/outline'; +import { PlusIcon, StarIcon } from '@heroicons/react/24/outline'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { selectReadonly } from '~/app/meta/metaSlice'; @@ -249,9 +245,7 @@ export default function Rollouts(props: RolloutsProps) {
-

- Rollouts -

+

Rollouts

Return boolean values based on rules you define

@@ -284,8 +278,7 @@ export default function Rollouts(props: RolloutsProps) { rule that matches will be applied.

- - You can re-arrange rollouts by clicking on a rollout header and{' '} + Rollouts can be rearranged by clicking on a rollout header and{' '} dragging and dropping it into place.

diff --git a/ui/src/app/flags/Evaluation.tsx b/ui/src/app/flags/rules/Rules.tsx similarity index 95% rename from ui/src/app/flags/Evaluation.tsx rename to ui/src/app/flags/rules/Rules.tsx index bdd7bae147..ba76fce268 100644 --- a/ui/src/app/flags/Evaluation.tsx +++ b/ui/src/app/flags/rules/Rules.tsx @@ -13,10 +13,11 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { InformationCircleIcon, PlusIcon } from '@heroicons/react/24/outline'; +import { PlusIcon } from '@heroicons/react/24/outline'; import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useNavigate, useOutletContext } from 'react-router-dom'; +import { FlagProps } from '~/app/flags/FlagProps'; import { selectReadonly } from '~/app/meta/metaSlice'; import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice'; import EmptyState from '~/components/EmptyState'; @@ -36,9 +37,8 @@ import { FlagType } from '~/types/Flag'; import { IRule, IRuleList } from '~/types/Rule'; import { ISegment, ISegmentList, SegmentOperatorType } from '~/types/Segment'; import { IVariant } from '~/types/Variant'; -import { FlagProps } from './FlagProps'; -export default function Evaluation() { +export default function Rules() { const { flag } = useOutletContext(); const [segments, setSegments] = useState([]); @@ -242,13 +242,10 @@ export default function Evaluation() { {/* rules */} -
+
-

- Rules -

-

+

Enable rich targeting and segmentation for evaluating your flags

@@ -280,8 +277,7 @@ export default function Evaluation() { first rule that matches will be applied.

- - You can re-arrange rules by clicking on a rule header and{' '} + Rules can be rearranged by clicking on a rule header and{' '} dragging and dropping{' '} it into place.

diff --git a/ui/src/app/flags/variants/Variants.tsx b/ui/src/app/flags/variants/Variants.tsx index 6fb6272577..e12e0a7a7e 100644 --- a/ui/src/app/flags/variants/Variants.tsx +++ b/ui/src/app/flags/variants/Variants.tsx @@ -1,6 +1,7 @@ import { PlusIcon } from '@heroicons/react/24/outline'; import { useRef, useState } from 'react'; import { useSelector } from 'react-redux'; +import { useOutletContext } from 'react-router-dom'; import { selectReadonly } from '~/app/meta/metaSlice'; import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice'; import EmptyState from '~/components/EmptyState'; @@ -15,11 +16,11 @@ import { IVariant } from '~/types/Variant'; type VariantsProps = { flag: IFlag; - flagChanged: () => void; + onFlagChange: () => void; }; -export default function Variants(props: VariantsProps) { - const { flag, flagChanged } = props; +export default function Variants() { + const { flag, onFlagChange } = useOutletContext(); const [showVariantForm, setShowVariantForm] = useState(false); const [editingVariant, setEditingVariant] = useState(null); @@ -47,7 +48,7 @@ export default function Variants(props: VariantsProps) { setOpen={setShowVariantForm} onSuccess={() => { setShowVariantForm(false); - flagChanged(); + onFlagChange(); }} /> @@ -71,18 +72,15 @@ export default function Variants(props: VariantsProps) { deleteVariant(namespace.key, flag.key, deletingVariant?.id ?? '') // TODO: Determine impact of blank ID param } onSuccess={() => { - flagChanged(); + onFlagChange(); }} /> {/* variants */} -
+
-

- Variants -

Return different values based on rules you define

@@ -108,7 +106,7 @@ export default function Variants(props: VariantsProps) {
)}
-
+
{flag.variants && flag.variants.length > 0 ? ( diff --git a/ui/src/app/segments/NewSegment.tsx b/ui/src/app/segments/NewSegment.tsx index 6832970d15..cc73c8c12f 100644 --- a/ui/src/app/segments/NewSegment.tsx +++ b/ui/src/app/segments/NewSegment.tsx @@ -14,9 +14,6 @@ export default function NewSegment() {
-

- Details -

Basic information about the segment.

diff --git a/ui/src/app/segments/Segment.tsx b/ui/src/app/segments/Segment.tsx index 98a0bc7004..3e1b8e29e1 100644 --- a/ui/src/app/segments/Segment.tsx +++ b/ui/src/app/segments/Segment.tsx @@ -234,12 +234,9 @@ export default function Segment() {
{/* segment details */} -
+
-

- Details -

Basic information about the segment

@@ -264,9 +261,9 @@ export default function Segment() {
-

+

Constraints -

+

Determine if a request matches a segment

diff --git a/ui/src/components/TabBar.tsx b/ui/src/components/TabBar.tsx index 71bd5d673b..dfb4ec677b 100644 --- a/ui/src/components/TabBar.tsx +++ b/ui/src/components/TabBar.tsx @@ -4,7 +4,6 @@ import { classNames } from '~/utils/helpers'; export interface Tab { name: string; to: string; - disabled?: boolean; } type TabBarProps = { @@ -18,32 +17,23 @@ export default function TabBar(props: TabBarProps) {
diff --git a/ui/src/components/flags/FlagForm.tsx b/ui/src/components/flags/FlagForm.tsx index 1d980d817c..9583c09346 100644 --- a/ui/src/components/flags/FlagForm.tsx +++ b/ui/src/components/flags/FlagForm.tsx @@ -187,7 +187,7 @@ export default function FlagForm(props: FlagFormProps) { />
-
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/ui/src/components/segments/SegmentForm.tsx b/ui/src/components/segments/SegmentForm.tsx index 25601f8f92..6eefe5f8cf 100644 --- a/ui/src/components/segments/SegmentForm.tsx +++ b/ui/src/components/segments/SegmentForm.tsx @@ -169,7 +169,7 @@ export default function SegmentForm(props: SegmentFormProps) { />
-
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/ui/tests/rules.spec.ts b/ui/tests/rules.spec.ts index c54e0b017c..316f2888d1 100644 --- a/ui/tests/rules.spec.ts +++ b/ui/tests/rules.spec.ts @@ -38,7 +38,7 @@ test.describe('Rules', () => { await page.reload(); await page.getByRole('link', { name: 'Flags' }).click(); await page.getByRole('link', { name: 'test-rule' }).click(); - await page.getByRole('link', { name: 'Evaluation' }).click(); + await page.getByRole('link', { name: 'Rules' }).click(); await page.getByRole('button', { name: 'New Rule' }).click(); await page.locator('#segmentKey-0-select-button').click(); await page.getByLabel('New Rule').getByText('Test Rule').click(); @@ -50,7 +50,7 @@ test.describe('Rules', () => { test('can update rule', async ({ page }) => { await page.getByRole('link', { name: 'test-rule' }).click(); - await page.getByRole('link', { name: 'Evaluation' }).click(); + await page.getByRole('link', { name: 'Rules' }).click(); await page .locator('input[name="rollouts\\.\\[0\\]\\.distribution\\.rollout"]') .click(); @@ -60,7 +60,10 @@ test.describe('Rules', () => { await page .locator('input[name="rollouts\\.\\[1\\]\\.distribution\\.rollout"]') .click(); - await page.getByRole('button', { name: 'Update' }).click(); + await page + .getByRole('listitem') + .getByRole('button', { name: 'Update' }) + .click(); await expect(page.getByText('Successfully updated rule')).toBeVisible(); }); }); @@ -83,20 +86,20 @@ test.describe('Rules - Read Only', () => { test('can not create rule', async ({ page }) => { await page.getByRole('link', { name: 'Flags' }).click(); await page.getByRole('link', { name: 'test-rule' }).click(); - await page.getByRole('link', { name: 'Evaluation' }).click(); + await page.getByRole('link', { name: 'Rules' }).click(); await expect(page.getByRole('button', { name: 'New Rule' })).toBeDisabled(); }); test('can not update rule', async ({ page }) => { await page.getByRole('link', { name: 'test-rule' }).click(); - await page.getByRole('link', { name: 'Evaluation' }).click(); + await page.getByRole('link', { name: 'Rules' }).click(); await page.getByTestId('rule-menu-button').click(); await expect(page.getByRole('link', { name: 'Edit' })).toBeHidden(); }); test('can not delete rule', async ({ page }) => { await page.getByRole('link', { name: 'test-rule' }).click(); - await page.getByRole('link', { name: 'Evaluation' }).click(); + await page.getByRole('link', { name: 'Rules' }).click(); await page.getByTestId('rule-menu-button').click(); await expect(page.getByRole('link', { name: 'Delete' })).toBeHidden(); });