Skip to content

Commit 09d18f5

Browse files
authored
feat(ui): debugger feature (#1233)
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
1 parent c8dc8f7 commit 09d18f5

File tree

18 files changed

+764
-3
lines changed

18 files changed

+764
-3
lines changed

ui/web-v2/src/assets/lang/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@
209209
"copy.copied": "Copied!",
210210
"copy.copyToClipboard": "Copy to clipboard",
211211
"created": "Created",
212+
"debugger.description": "The debugger can verify what variation will be assigned to a specific end-user under different conditions in real-time.",
213+
"debugger.title": "Debugger",
212214
"description": "Description",
213215
"disabled": "Disabled",
214216
"enabled": "Enabled",
@@ -542,6 +544,7 @@
542544
"sideMenu.adminSettings": "Admin Settings",
543545
"sideMenu.apiKeys": "API Keys",
544546
"sideMenu.auditLog": "Audit Logs",
547+
"sideMenu.debugger": "Debugger",
545548
"sideMenu.documentation": "Documentation",
546549
"sideMenu.experiments": "Experiments",
547550
"sideMenu.featureFlags": "Feature Flags",

ui/web-v2/src/assets/lang/ja.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@
209209
"copy.copied": "コピーしました",
210210
"copy.copyToClipboard": "クリップボードにコピー",
211211
"created": "作成",
212+
"debugger.description": "デバッガーは、異なる条件下で特定のエンドユーザーにどのバリエーションが割り当てられるかをリアルタイムで検証できます。",
213+
"debugger.title": "デバッガー",
212214
"description": "説明",
213215
"disabled": "無効",
214216
"enabled": "有効",
@@ -542,6 +544,7 @@
542544
"sideMenu.adminSettings": "管理者設定",
543545
"sideMenu.apiKeys": "APIキー",
544546
"sideMenu.auditLog": "監視ログ",
547+
"sideMenu.debugger": "デバッガー",
545548
"sideMenu.documentation": "ドキュメント",
546549
"sideMenu.experiments": "エクスペリメント",
547550
"sideMenu.featureFlags": "フィーチャーフラグ",
Lines changed: 15 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { FC, memo, useCallback, useEffect } from 'react';
2+
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
3+
import { useIntl } from 'react-intl';
4+
import { messages } from '../../lang/messages';
5+
import { Select } from '../Select';
6+
import { useCurrentEnvironment } from '../../modules/me';
7+
import { AppDispatch } from '../../store';
8+
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
9+
import {
10+
listFeatures,
11+
selectAll as selectAllFeatures
12+
} from '../../modules/features';
13+
import { ListFeaturesRequest } from '../../proto/feature/service_pb';
14+
import { AppState } from '../../modules';
15+
import { Feature } from '../../proto/feature/feature_pb';
16+
import { PlusIcon, TrashIcon } from '@heroicons/react/outline';
17+
import { v4 as uuid } from 'uuid';
18+
19+
export interface DebuggerEvaluateFormProps {
20+
onSubmit: () => void;
21+
}
22+
23+
export const DebuggerEvaluateForm: FC<DebuggerEvaluateFormProps> = memo(
24+
({ onSubmit }) => {
25+
const currentEnvironment = useCurrentEnvironment();
26+
const dispatch = useDispatch<AppDispatch>();
27+
const { formatMessage: f } = useIntl();
28+
29+
const methods = useFormContext();
30+
const {
31+
register,
32+
control,
33+
formState: { errors, isValid, isSubmitting }
34+
} = methods;
35+
36+
const {
37+
append: appendUserAttributes,
38+
remove: removeUserAttribute,
39+
fields: userAttributes
40+
} = useFieldArray({
41+
control,
42+
name: 'userAttributes'
43+
});
44+
45+
const features = useSelector<AppState, Feature.AsObject[]>(
46+
(state) => selectAllFeatures(state.features),
47+
shallowEqual
48+
);
49+
50+
useEffect(() => {
51+
dispatch(
52+
listFeatures({
53+
environmentNamespace: currentEnvironment.id,
54+
pageSize: 0,
55+
cursor: '',
56+
tags: [],
57+
searchKeyword: null,
58+
maintainerId: null,
59+
orderBy: ListFeaturesRequest.OrderBy.DEFAULT,
60+
orderDirection: ListFeaturesRequest.OrderDirection.ASC,
61+
archived: false
62+
})
63+
);
64+
}, []);
65+
66+
const handleAddAttribute = () => {
67+
appendUserAttributes({
68+
id: uuid(),
69+
key: '',
70+
value: ''
71+
});
72+
};
73+
74+
const handleDeleteAttribute = useCallback((index) => {
75+
removeUserAttribute(index);
76+
}, []);
77+
78+
return (
79+
<form className="flex flex-col space-y-6">
80+
<div className="">
81+
<label htmlFor="flag">
82+
<span className="input-label">Flag</span>
83+
</label>
84+
<Controller
85+
name="flag"
86+
control={control}
87+
render={({ field }) => {
88+
const selectedOptions = features
89+
.filter((feature) => field.value.includes(feature.id))
90+
.map((feature) => ({
91+
label: feature.name,
92+
value: feature.id
93+
}));
94+
95+
return (
96+
<Select
97+
isMulti
98+
options={features.map((feature) => ({
99+
label: feature.name,
100+
value: feature.id
101+
}))}
102+
value={selectedOptions} // Ensure the value prop is set correctly
103+
onChange={(selected) => {
104+
const newValues = selected
105+
? selected.map((o) => o.value)
106+
: [];
107+
field.onChange(newValues);
108+
}}
109+
placeholder="Select a feature flag"
110+
disabled={isSubmitting}
111+
/>
112+
);
113+
}}
114+
/>
115+
<p className="input-error">
116+
{errors.flag && <span role="alert">{errors.flag.message}</span>}
117+
</p>
118+
</div>
119+
<div className="">
120+
<label htmlFor="userId">
121+
<span className="input-label">User ID</span>
122+
</label>
123+
<div className="mt-1">
124+
<input
125+
{...register('userId')}
126+
placeholder="Enter a user ID"
127+
type="text"
128+
id="userId"
129+
className="input-text w-full"
130+
disabled={isSubmitting}
131+
/>
132+
<p className="input-error">
133+
{errors.userId && (
134+
<span role="alert">{errors.userId.message}</span>
135+
)}
136+
</p>
137+
</div>
138+
</div>
139+
<div className="space-y-4">
140+
{userAttributes.length > 0 && (
141+
<div>
142+
<div>
143+
<p>User Attributes</p>
144+
<p className="text-sm">
145+
<a
146+
href="https://docs.bucketeer.io/feature-flags/creating-feature-flags/targeting#user-attributes"
147+
target="_blank"
148+
rel="noreferrer"
149+
className="link"
150+
>
151+
{f(messages.readMore)}
152+
</a>
153+
</p>
154+
</div>
155+
<div>
156+
{userAttributes.map((attr, index) => (
157+
<div key={attr.id} className="flex space-x-4 mt-4 items-end">
158+
<div className="flex flex-col flex-1">
159+
<label htmlFor="key">
160+
<span className="input-label">Key</span>
161+
</label>
162+
<input
163+
{...register(`userAttributes.${index}.key`)}
164+
type="text"
165+
id="key"
166+
className="input-text w-full"
167+
/>
168+
</div>
169+
<div className="flex flex-col flex-1">
170+
<label htmlFor="value">
171+
<span className="input-label">Value</span>
172+
</label>
173+
<input
174+
{...register(`userAttributes.${index}.value`)}
175+
type="text"
176+
id="value"
177+
className="input-text w-full"
178+
/>
179+
</div>
180+
<TrashIcon
181+
width={18}
182+
className="cursor-pointer text-gray-400 mb-3"
183+
onClick={() => handleDeleteAttribute(index)}
184+
/>
185+
</div>
186+
))}
187+
</div>
188+
</div>
189+
)}
190+
<button
191+
className="flex whitespace-nowrap space-x-2 text-primary max-w-min py-2 items-center"
192+
type="button"
193+
onClick={handleAddAttribute}
194+
>
195+
<PlusIcon width={18} />
196+
<span>Add a user attribute</span>
197+
</button>
198+
</div>
199+
<div className="flex">
200+
<button
201+
type="button"
202+
className="btn-submit"
203+
disabled={!isValid || isSubmitting}
204+
onClick={onSubmit}
205+
>
206+
Evaluate
207+
</button>
208+
</div>
209+
</form>
210+
);
211+
}
212+
);

0 commit comments

Comments
 (0)