Skip to content

Commit 4bc06a7

Browse files
committed
wip
1 parent 341d99e commit 4bc06a7

File tree

8 files changed

+264
-63
lines changed

8 files changed

+264
-63
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@
473473
"notification.sort.nameZa": "Name Z-A",
474474
"notification.sort.newest": "Newest",
475475
"notification.sort.oldest": "Oldest",
476+
"notification.tagsTooltipMessage": "The configured tags in the Feature Flag when was created. It will only notify you if the tags match the tag configured in the Feature Flag. When empty, it will notify you of any changes to all the flags.",
476477
"notification.update.header.description": "A notification lets you know when someone adds or updates something on the admin console and operational tasks status.",
477478
"notification.update.header.title": "Update the notification",
478479
"progressiveRolloutWarningMessages.alreadyProgressiveRollout": "Cannot execute more than one progressive rollout at the same time.",
@@ -633,4 +634,4 @@
633634
"urlCode": "URL code",
634635
"warning": "Warning",
635636
"yes": "Yes"
636-
}
637+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@
473473
"notification.sort.nameZa": "名前 Z-A",
474474
"notification.sort.newest": "新しい順",
475475
"notification.sort.oldest": "古い順",
476+
"notification.tagsTooltipMessage": "タグはフラグの作成時に設定されたタグです。タグがフラグで設定されたタグと一致する場合にのみ通知されます。空の場合は、全てのフラグが更新時に通知されます。",
476477
"notification.update.header.description": "通知機能を使用することによって、管理コンソールでの追加・更新をした時や、運用タスクの状況を確認することができます。",
477478
"notification.update.header.title": "通知の更新",
478479
"progressiveRolloutWarningMessages.alreadyProgressiveRollout": "同時に複数のプログレッシブロールアウトを実行することはできません。",

ui/web-v2/src/components/CheckBoxList/index.tsx

Lines changed: 144 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1-
import { FC, memo, useState } from 'react';
1+
import { FC, memo, useEffect, useState } from 'react';
22

33
import { CheckBox } from '../CheckBox';
4+
import { useIntl } from 'react-intl';
5+
import { messages } from '../../lang/messages';
6+
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
7+
import { AppState } from '../../modules';
8+
import { Tag } from '../../proto/tag/tag_pb';
9+
import { listTags, selectAll as selectAllTags } from '../../modules/tags';
10+
import { Subscription } from '../../proto/notification/subscription_pb';
11+
import { Select } from '../Select';
12+
import { Controller, useFormContext } from 'react-hook-form';
13+
import { AppDispatch } from '../../store';
14+
import { ListTagsRequest } from '../../proto/tag/service_pb';
15+
import { useCurrentEnvironment } from '../../modules/me';
16+
import { HoverPopover } from '../HoverPopover';
17+
import { classNames } from '../../utils/css';
18+
import { InformationCircleIcon } from '@heroicons/react/outline';
419

520
export interface Option {
621
value: string;
@@ -17,6 +32,25 @@ export interface CheckBoxListProps {
1732

1833
export const CheckBoxList: FC<CheckBoxListProps> = memo(
1934
({ onChange, options, defaultValues, disabled }) => {
35+
const { formatMessage: f } = useIntl();
36+
const methods = useFormContext();
37+
const {
38+
control,
39+
formState: { errors }
40+
} = methods;
41+
42+
const dispatch = useDispatch<AppDispatch>();
43+
const currentEnvironment = useCurrentEnvironment();
44+
45+
const tagsList = useSelector<AppState, Tag.AsObject[]>(
46+
(state) => selectAllTags(state.tags),
47+
shallowEqual
48+
);
49+
50+
const featureFlagTagsList = tagsList.filter(
51+
(tag) => tag.entityType === Tag.EntityType.FEATURE_FLAG
52+
);
53+
2054
const [checkedItems] = useState(() => {
2155
const items = new Map();
2256
defaultValues &&
@@ -26,6 +60,19 @@ export const CheckBoxList: FC<CheckBoxListProps> = memo(
2660
return items;
2761
});
2862

63+
useEffect(() => {
64+
dispatch(
65+
listTags({
66+
environmentId: currentEnvironment.id,
67+
pageSize: 0,
68+
cursor: '',
69+
orderBy: ListTagsRequest.OrderBy.DEFAULT,
70+
orderDirection: ListTagsRequest.OrderDirection.ASC,
71+
searchKeyword: null
72+
})
73+
);
74+
}, [dispatch]);
75+
2976
const handleOnChange = (value: string, checked: boolean) => {
3077
if (checked) {
3178
checkedItems.set(value, value);
@@ -45,31 +92,104 @@ export const CheckBoxList: FC<CheckBoxListProps> = memo(
4592
<div className="divide-y divide-gray-300">
4693
{options.map((item, index) => {
4794
return (
48-
<div
49-
key={item.label}
50-
className="relative flex items-start py-4"
51-
>
52-
<div className="min-w-0 flex-1 text-sm">
53-
<label htmlFor={`id_${index}`} key={`key_${index}`}>
54-
<p className="text-sm font-medium text-gray-700">
55-
{item.label}
56-
</p>
57-
{item.description && (
58-
<p className="text-sm text-gray-500">
59-
{item.description}
95+
<div key={item.label}>
96+
<div className="relative flex items-start py-4">
97+
<div className="min-w-0 flex-1 text-sm">
98+
<label htmlFor={`id_${index}`} key={`key_${index}`}>
99+
<p className="text-sm font-medium text-gray-700">
100+
{item.label}
60101
</p>
61-
)}
62-
</label>
63-
</div>
64-
<div className="ml-3 flex items-center h-5">
65-
<CheckBox
66-
id={`id_${index}`}
67-
value={item.value}
68-
onChange={handleOnChange}
69-
defaultChecked={checkedItems.has(item.value)}
70-
disabled={disabled}
71-
/>
102+
{item.description && (
103+
<p className="text-sm text-gray-500">
104+
{item.description}
105+
</p>
106+
)}
107+
</label>
108+
</div>
109+
<div className="ml-3 flex items-center h-5">
110+
<CheckBox
111+
id={`id_${index}`}
112+
value={item.value}
113+
onChange={handleOnChange}
114+
defaultChecked={checkedItems.has(item.value)}
115+
disabled={disabled}
116+
/>
117+
</div>
72118
</div>
119+
{item.value ===
120+
Subscription.SourceType.DOMAIN_EVENT_FEATURE.toString() &&
121+
checkedItems.has(
122+
Subscription.SourceType.DOMAIN_EVENT_FEATURE.toString()
123+
) && (
124+
<div className="mb-4">
125+
<div className="flex space-x-1 items-center">
126+
<label htmlFor="tags">
127+
<span className="input-label">
128+
{f(messages.tags.title)}
129+
</span>
130+
</label>
131+
<HoverPopover
132+
render={() => {
133+
return (
134+
<div
135+
className={classNames(
136+
'border shadow-sm bg-white text-gray-500 p-1',
137+
'text-xs rounded whitespace-normal break-words w-64'
138+
)}
139+
>
140+
{f(messages.notification.tagsTooltipMessage)}
141+
</div>
142+
);
143+
}}
144+
>
145+
<div
146+
className={classNames(
147+
'hover:text-gray-500 mb-[2px]'
148+
)}
149+
>
150+
<InformationCircleIcon
151+
className="w-5 h-5 text-gray-400"
152+
aria-hidden="true"
153+
/>
154+
</div>
155+
</HoverPopover>
156+
</div>
157+
<Controller
158+
name="featureFlagTagsList"
159+
control={control}
160+
render={({ field }) => {
161+
return (
162+
<Select
163+
isMulti
164+
value={field.value?.map((tag: string) => {
165+
return {
166+
value: tag,
167+
label: tag
168+
};
169+
})}
170+
options={featureFlagTagsList.map((tag) => ({
171+
label: tag.name,
172+
value: tag.name
173+
}))}
174+
onChange={(options: Option[]) => {
175+
field.onChange(options.map((o) => o.value));
176+
}}
177+
closeMenuOnSelect={false}
178+
placeholder={f(messages.tags.tagsPlaceholder)}
179+
disabled={disabled}
180+
/>
181+
);
182+
}}
183+
/>
184+
{errors.tags && (
185+
<p className="input-error">
186+
{errors.tags && (
187+
<span role="alert">{errors.tags.message}</span>
188+
)}
189+
</p>
190+
)}
191+
</div>
192+
)}
73193
</div>
74194
);
75195
})}

ui/web-v2/src/components/NotificationList/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export const NotificationList: FC<NotificationListProps> = memo(
5252
shallowEqual
5353
);
5454

55+
console.log({
56+
notificationList
57+
});
58+
5559
return (
5660
<div className="w-full">
5761
<div className="flex items-stretch mb-8 text-sm">

ui/web-v2/src/lang/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,6 +2389,11 @@ export const messages = {
23892389
})
23902390
}
23912391
},
2392+
tagsTooltipMessage: defineMessage({
2393+
id: 'notification.tagsTooltipMessage',
2394+
defaultMessage:
2395+
'The configured tags in the Feature Flag when was created. It will only notify you if the tags match the tag configured in the Feature Flag. When empty, it will notify you of any changes to all the flags.'
2396+
}),
23922397
slackIncomingWebhookUrl: defineMessage({
23932398
id: 'notification.slackIncomingWebhookUrl',
23942399
defaultMessage: 'Slack incoming webhook URL'

ui/web-v2/src/modules/notifications.ts

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
DisableSubscriptionCommand,
1414
AddSourceTypesCommand,
1515
DeleteSourceTypesCommand,
16-
RenameSubscriptionCommand
16+
RenameSubscriptionCommand,
17+
UpdateSubscriptionFeatureFlagTagsCommand
1718
} from '../proto/notification/command_pb';
1819
import {
1920
Recipient,
@@ -88,6 +89,7 @@ export interface CreateNotificationParams {
8889
Subscription.SourceTypeMap[keyof Subscription.SourceTypeMap]
8990
>;
9091
webhookUrl: string;
92+
featureFlagTagsList: string[];
9193
}
9294

9395
export const createNotification = createAsyncThunk<
@@ -98,6 +100,9 @@ export const createNotification = createAsyncThunk<
98100
const cmd = new CreateSubscriptionCommand();
99101
cmd.setName(params.name);
100102
cmd.setSourceTypesList(params.sourceTypes);
103+
params.featureFlagTagsList &&
104+
params.featureFlagTagsList.length > 0 &&
105+
cmd.setFeatureFlagTagsList(params.featureFlagTagsList);
101106

102107
const recipient = new Recipient();
103108
recipient.setType(Recipient.Type.SLACKCHANNEL);
@@ -115,6 +120,7 @@ export const createNotification = createAsyncThunk<
115120
const request = new CreateSubscriptionRequest();
116121
request.setEnvironmentId(params.environmentId);
117122
request.setCommand(cmd);
123+
118124
await subscriptionGrpc.createSubscription(request);
119125
});
120126

@@ -128,6 +134,7 @@ export interface UpdateNotificationParams {
128134
sourceTypes: Array<
129135
Subscription.SourceTypeMap[keyof Subscription.SourceTypeMap]
130136
>;
137+
featureFlagTagsList: string[];
131138
}
132139

133140
export const updateNotification = createAsyncThunk<
@@ -145,23 +152,35 @@ export const updateNotification = createAsyncThunk<
145152
request.setRenameSubscriptionCommand(cmd);
146153
}
147154

148-
if (params.sourceTypes) {
149-
const addList = params.sourceTypes.filter(
150-
(type) => !params.currentSourceTypes.includes(type)
151-
);
152-
if (addList.length > 0) {
153-
const cmd = new AddSourceTypesCommand();
154-
cmd.setSourceTypesList(addList);
155-
request.setAddSourceTypesCommand(cmd);
156-
}
157-
const deleteList = params.currentSourceTypes.filter(
158-
(type) => !params.sourceTypes.includes(type)
159-
);
160-
if (deleteList.length > 0) {
161-
const cmd = new DeleteSourceTypesCommand();
162-
cmd.setSourceTypesList(deleteList);
163-
request.setDeleteSourceTypesCommand(cmd);
164-
}
155+
const addList = params.sourceTypes.filter(
156+
(type) => !params.currentSourceTypes.includes(type)
157+
);
158+
// DOMAIN_EVENT_FEATURE must be included in sourceTypes if featureFlagTagsList is set
159+
// if (
160+
// params.featureFlagTagsList &&
161+
// !params.sourceTypes.includes(Subscription.SourceType.DOMAIN_EVENT_FEATURE)
162+
// ) {
163+
// addList.push(Subscription.SourceType.DOMAIN_EVENT_FEATURE);
164+
// }
165+
if (addList.length > 0) {
166+
const cmd = new AddSourceTypesCommand();
167+
cmd.setSourceTypesList(addList);
168+
request.setAddSourceTypesCommand(cmd);
169+
}
170+
const deleteList = params.currentSourceTypes.filter(
171+
(type) => !params.sourceTypes.includes(type)
172+
);
173+
if (deleteList.length > 0) {
174+
const cmd = new DeleteSourceTypesCommand();
175+
cmd.setSourceTypesList(deleteList);
176+
request.setDeleteSourceTypesCommand(cmd);
177+
}
178+
console.log({ params, addList, deleteList });
179+
180+
if (params.featureFlagTagsList) {
181+
const cmd = new UpdateSubscriptionFeatureFlagTagsCommand();
182+
cmd.setFeatureFlagTagsList(params.featureFlagTagsList);
183+
request.setUpdateSubscriptionFeatureTagsCommand(cmd);
165184
}
166185

167186
await subscriptionGrpc.updateSubscription(request);

ui/web-v2/src/pages/notification/formSchema.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '../../constants/notification';
88
import { intl } from '../../lang';
99
import { messages } from '../../lang/messages';
10+
import { Subscription } from '../../proto/notification/subscription_pb';
1011

1112
yup.setLocale(yupLocale);
1213

@@ -18,15 +19,40 @@ const sourceTypesSchema = yup
1819
NOTIFICATION_SOURCE_TYPES_MIN_LENGTH,
1920
intl.formatMessage(messages.input.error.minSelectOptionLength)
2021
);
22+
2123
const webhookUrlSchema = yup.string().required().url();
24+
const featureFlagTagsListSchema = yup
25+
.array()
26+
.of(yup.string())
27+
.test(
28+
'tags-required-for-flag',
29+
intl.formatMessage(messages.input.error.required),
30+
function (featureFlagTagsList) {
31+
const { sourceTypes } = this.parent;
32+
const hasFlag = sourceTypes?.includes(
33+
Subscription.SourceType.DOMAIN_EVENT_FEATURE.toString()
34+
);
35+
36+
// If 'Flag' is selected, ensure tags are present and not empty.
37+
if (
38+
hasFlag &&
39+
(!featureFlagTagsList || featureFlagTagsList.length === 0)
40+
) {
41+
return false;
42+
}
43+
return true;
44+
}
45+
);
2246

2347
export const addFormSchema = yup.object().shape({
2448
name: nameSchema,
2549
sourceTypes: sourceTypesSchema,
26-
webhookUrl: webhookUrlSchema
50+
webhookUrl: webhookUrlSchema,
51+
featureFlagTagsList: featureFlagTagsListSchema
2752
});
2853

2954
export const updateFormSchema = yup.object().shape({
3055
name: nameSchema,
31-
sourceTypes: sourceTypesSchema
56+
sourceTypes: sourceTypesSchema,
57+
featureFlagTagsList: featureFlagTagsListSchema
3258
});

0 commit comments

Comments
 (0)