Skip to content

Commit

Permalink
feat: create eval rollout threshold (#1835)
Browse files Browse the repository at this point in the history
* feat(wip): ui rollout threshold crud

* feat(wip): show new rollout button

* chore: add type to flag table

* feat(wip): threshold rule form

* feat(wip): rollout thresholds

* chore: rename to 'rules' for now

* chore: rm un-needed prop

* chore: fix small issues

* chore: extract some more constants

* feat: add missing description, set max/min for threshold client side

* fix: typing

* chore: type the form values

* chore: move value to common form

* chore: disable evaluation tab for boolean flag
  • Loading branch information
markphelps committed Jul 7, 2023
1 parent af0149c commit f744d0a
Show file tree
Hide file tree
Showing 21 changed files with 639 additions and 106 deletions.
8 changes: 5 additions & 3 deletions ui/src/app/flags/EditFlag.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useOutletContext } from 'react-router-dom';
import FlagForm from '~/components/flags/FlagForm';
import MoreInfo from '~/components/MoreInfo';
import { FlagType } from '~/types/Flag';
import { FlagType, flagTypeToLabel } 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<FlagProps>();

const flagTypeToLabel = (t: string) => FlagType[t as keyof typeof FlagType];

return (
<>
<div className="flex flex-col">
Expand All @@ -36,6 +35,9 @@ export default function EditFlag() {
{flagTypeToLabel(flag.type) === FlagType.VARIANT_FLAG_TYPE && (
<Variants flag={flag} flagChanged={onFlagChange} />
)}
{flagTypeToLabel(flag.type) === FlagType.BOOLEAN_FLAG_TYPE && (
<Rollouts flag={flag} flagChanged={onFlagChange} />
)}
</div>
</>
);
Expand Down
6 changes: 2 additions & 4 deletions ui/src/app/flags/Evaluation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,7 @@ export default function Evaluation() {
handleDelete={() =>
deleteRule(namespace.key, flag.key, deletingRule?.id ?? '')
}
onSuccess={() => {
incrementRulesVersion();
}}
onSuccess={incrementRulesVersion}
/>
</Modal>

Expand All @@ -203,7 +201,7 @@ export default function Evaluation() {
rank={(rules?.length || 0) + 1}
segments={segments}
setOpen={setShowRuleForm}
rulesChanged={incrementRulesVersion}
onSuccess={incrementRulesVersion}
/>
</Slideover>

Expand Down
25 changes: 13 additions & 12 deletions ui/src/app/flags/Flag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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 { IFlag } from '~/types/Flag';
import { FlagType, flagTypeToLabel, IFlag } from '~/types/Flag';

export default function Flag() {
let { flagKey } = useParams();
Expand All @@ -47,17 +47,6 @@ export default function Flag() {
setFlagVersion(flagVersion + 1);
};

const tabs = [
{
name: 'Details',
to: ''
},
{
name: 'Evaluation',
to: 'evaluation'
}
];

useEffect(() => {
if (!flagKey) return;

Expand All @@ -72,6 +61,18 @@ export default function Flag() {

if (!flag) return <Loading />;

const tabs = [
{
name: 'Details',
to: ''
},
{
name: 'Evaluation',
to: 'evaluation',
disabled: flagTypeToLabel(flag.type) === FlagType.BOOLEAN_FLAG_TYPE
}
];

return (
<>
{/* flag delete modal */}
Expand Down
151 changes: 151 additions & 0 deletions ui/src/app/flags/rollouts/Rollouts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { PlusIcon } from '@heroicons/react/24/outline';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import EmptyState from '~/components/EmptyState';
import Button from '~/components/forms/buttons/Button';
import Modal from '~/components/Modal';
import DeletePanel from '~/components/panels/DeletePanel';
import RolloutForm from '~/components/rollouts/RolloutForm';
import Slideover from '~/components/Slideover';
import { deleteRollout, listRollouts } from '~/data/api';
import { IFlag } from '~/types/Flag';
import { IRollout, IRolloutList } from '~/types/Rollout';

type RolloutsProps = {
flag: IFlag;
flagChanged: () => void;
};

export default function Rollouts(props: RolloutsProps) {
const { flag, flagChanged } = props;

const [rollouts, setRollouts] = useState<IRollout[]>([]);

const [rolloutsVersion, setRolloutsVersion] = useState(0);
const [showRolloutForm, setShowRolloutForm] = useState<boolean>(false);

const [editingRollout, setEditingRollout] = useState<IRollout | null>(null);
const [showDeleteRolloutModal, setShowDeleteRolloutModal] =
useState<boolean>(false);
const [deletingRollout] = useState<IRollout | null>(null);

const rolloutFormRef = useRef(null);

const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);

const loadData = useCallback(async () => {
// TODO: load segments

const rolloutList = (await listRollouts(
namespace.key,
flag.key
)) as IRolloutList;

setRollouts(rolloutList.rules);
}, [namespace.key, flag.key]);

const incrementRolloutsVersion = () => {
setRolloutsVersion(rolloutsVersion + 1);
};

useEffect(() => {
loadData();
}, [loadData, rolloutsVersion]);

return (
<>
{/* rollout edit form */}
<Slideover
open={showRolloutForm}
setOpen={setShowRolloutForm}
ref={rolloutFormRef}
>
<RolloutForm
flag={flag}
rank={(rollouts?.length || 0) + 1}
rollout={editingRollout || undefined}
setOpen={setShowRolloutForm}
onSuccess={() => {
setShowRolloutForm(false);
incrementRolloutsVersion();
flagChanged();
}}
/>
</Slideover>

{/* rollout delete modal */}
<Modal open={showDeleteRolloutModal} setOpen={setShowDeleteRolloutModal}>
<DeletePanel
panelMessage={
<>
Are you sure you want to delete this rule at
<span className="font-medium text-violet-500">
{' '}
position {deletingRollout?.rank}
</span>
? This action cannot be undone.
</>
}
panelType="Rollout"
setOpen={setShowDeleteRolloutModal}
handleDelete={
() =>
deleteRollout(namespace.key, flag.key, deletingRollout?.id ?? '') // TODO: Determine impact of blank ID param
}
onSuccess={flagChanged}
/>
</Modal>

{/* rollouts */}
<div className="mt-10">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-lg font-medium leading-6 text-gray-900">
Rollouts
</h1>
<p className="mt-1 text-sm text-gray-500">
Return boolean values based on rules you define
</p>
</div>
{rollouts && rollouts.length > 0 && (
<div className="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<Button
primary
type="button"
disabled={readOnly}
title={readOnly ? 'Not allowed in Read-Only mode' : undefined}
onClick={() => {
setEditingRollout(null);
setShowRolloutForm(true);
}}
>
<PlusIcon
className="-ml-1.5 mr-1 h-5 w-5 text-white"
aria-hidden="true"
/>
<span>New Rollout</span>
</Button>
</div>
)}
</div>
<div className="mt-10">
{rollouts && rollouts.length > 0 ? (
<></>
) : (
<EmptyState
text="New Rollout"
disabled={readOnly}
onClick={() => {
setEditingRollout(null);
setShowRolloutForm(true);
}}
/>
)}
</div>
</div>
</>
);
}
8 changes: 2 additions & 6 deletions ui/src/app/segments/Segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import { useSuccess } from '~/data/hooks/success';
import { useTimezone } from '~/data/hooks/timezone';
import {
ComparisonType,
ConstraintOperators,
constraintOperatorToLabel,
constraintTypeToLabel,
IConstraint
} from '~/types/Constraint';
import { ISegment } from '~/types/Segment';
Expand Down Expand Up @@ -83,11 +84,6 @@ export default function Segment() {
});
}, [segmentVersion, namespace.key, segmentKey, clearError, setError]);

const constraintTypeToLabel = (t: string) =>
ComparisonType[t as keyof typeof ComparisonType];

const constraintOperatorToLabel = (o: string) => ConstraintOperators[o];

const constraintFormRef = useRef(null);

if (!segment) return <Loading />;
Expand Down
44 changes: 27 additions & 17 deletions ui/src/components/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { classNames } from '~/utils/helpers';
export interface Tab {
name: string;
to: string;
disabled?: boolean;
}

type TabBarProps = {
Expand All @@ -17,23 +18,32 @@ export default function TabBar(props: TabBarProps) {
<div className="mt-3 flex flex-row sm:mt-5">
<div className="border-b-2 border-gray-200">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<NavLink
end
key={tab.name}
to={tab.to}
className={({ isActive }) =>
classNames(
isActive
? 'text-violet-600 border-violet-500'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium'
)
}
>
{tab.name}
</NavLink>
))}
{tabs.map((tab) =>
tab.disabled ? (
<a
key={tab.name}
className="cursor-not-allowed whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium text-gray-500"
>
{tab.name}
</a>
) : (
<NavLink
end
key={tab.name}
to={tab.to}
className={({ isActive }) =>
classNames(
isActive
? 'text-violet-600 border-violet-500'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium'
)
}
>
{tab.name}
</NavLink>
)
)}
</nav>
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion ui/src/components/flags/FlagTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import Pagination from '~/components/Pagination';
import Searchbox from '~/components/Searchbox';
import { useTimezone } from '~/data/hooks/timezone';
import { IFlag } from '~/types/Flag';
import { FlagType, IFlag } from '~/types/Flag';
import { truncateKey } from '~/utils/helpers';

type FlagTableProps = {
Expand Down Expand Up @@ -83,6 +83,14 @@ export default function FlagTable(props: FlagTableProps) {
className: 'whitespace-nowrap py-4 px-3 text-sm'
}
}),
columnHelper.accessor('type', {
header: 'Type',
cell: (info) =>
FlagType[info.getValue() as unknown as keyof typeof FlagType],
meta: {
className: 'whitespace-nowrap py-4 px-3 text-sm text-gray-600'
}
}),
columnHelper.accessor('description', {
header: 'Description',
cell: (info) => info.getValue(),
Expand Down
9 changes: 4 additions & 5 deletions ui/src/components/forms/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline';
import { useField } from 'formik';
import { useState } from 'react';
import { classNames } from '~/utils/helpers';
import { ISelectable } from './Listbox';

type ComboboxProps<T extends ISelectable> = {
type ComboboxProps<T extends IFilterable> = {
id: string;
name: string;
placeholder?: string;
Expand All @@ -15,14 +16,12 @@ type ComboboxProps<T extends ISelectable> = {
className?: string;
};

export interface ISelectable {
key: string;
export interface IFilterable extends ISelectable {
status?: 'active' | 'inactive';
filterValue: string;
displayValue: string;
}

export default function Combobox<T extends ISelectable>(
export default function Combobox<T extends IFilterable>(
props: ComboboxProps<T>
) {
const {
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/forms/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function Select(props: SelectProps) {
<select
{...field}
id={id}
name={name}
className={`${className} block rounded-md py-2 pl-3 pr-10 text-base text-gray-900 bg-gray-50 border-gray-300 focus:outline-none focus:ring-violet-300 focus:border-violet-300 sm:text-sm`}
value={value}
onChange={onChange || field.onChange}
Expand Down
Loading

0 comments on commit f744d0a

Please sign in to comment.