Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit d11deb1

Browse files
committed
fix: Search for JSON View
1 parent e0da498 commit d11deb1

File tree

5 files changed

+177
-189
lines changed

5 files changed

+177
-189
lines changed

src/components/Navbar/components/CorrelationIcon.tsx

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,29 @@ export const CorrelationIcon = forwardRef<
77
strokeWidth?: number;
88
}
99
>(({ stroke, strokeWidth }, ref) => (
10-
<svg ref={ref} height="1.2rem" width="1.2rem" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
11-
<path
12-
d="M13.3335 17.3335L14.6669 18.6669C15.0205 19.0205 15.5001 19.2192 16.0002 19.2192C16.5003 19.2192 16.9799 19.0205 17.3335 18.6669L22.6669 13.3335C23.0205 12.9799 23.2192 12.5003 23.2192 12.0002C23.2192 11.5001 23.0205 11.0205 22.6669 10.6669L17.3335 5.33353C16.9799 4.97991 16.5003 4.78125 16.0002 4.78125C15.5001 4.78125 15.0205 4.97991 14.6669 5.33353L9.33353 10.6669C8.97991 11.0205 8.78125 11.5001 8.78125 12.0002C8.78125 12.5003 8.97991 12.9799 9.33353 13.3335L10.6669 14.6669"
13-
stroke={stroke}
14-
strokeWidth={strokeWidth}
15-
strokeLinecap="round"
16-
strokeLinejoin="round"
17-
/>
18-
<path
19-
d="M10.6669 6.66687L9.33353 5.33353C8.97991 4.97991 8.5003 4.78125 8.0002 4.78125C7.5001 4.78125 7.02049 4.97991 6.66687 5.33353L1.33353 10.6669C0.979912 11.0205 0.78125 11.5001 0.78125 12.0002C0.78125 12.5003 0.979912 12.9799 1.33353 13.3335L6.66687 18.6669C7.02049 19.0205 7.5001 19.2192 8.0002 19.2192C8.5003 19.2192 8.97991 19.0205 9.33353 18.6669L14.6669 13.3335C15.0205 12.9799 15.2192 12.5003 15.2192 12.0002C15.2192 11.5001 15.0205 11.0205 14.6669 10.6669L13.3335 9.33353"
20-
stroke={stroke}
21-
strokeWidth={strokeWidth}
22-
strokeLinecap="round"
23-
strokeLinejoin="round"
24-
/>
25-
<rect x="14" y="14" width="9.61539" height="10" rx="4.80769" fill="#F8F9FA" />
26-
<path
27-
d="M17.0771 22.4617V17.077C17.0771 16.669 17.2392 16.2777 17.5278 15.9892C17.8163 15.7007 18.2076 15.5386 18.6156 15.5386H18.8079C19.1649 15.5386 19.5073 15.6804 19.7598 15.9329C20.0122 16.1853 20.1541 16.5277 20.1541 16.8847C20.1541 17.2418 20.0122 17.5841 19.7598 17.8366C19.5073 18.0891 19.1649 18.2309 18.8079 18.2309M18.8079 18.2309H18.6156M18.8079 18.2309C19.1502 18.2309 19.4849 18.3324 19.7695 18.5226C20.0541 18.7127 20.2759 18.9831 20.4069 19.2993C20.5379 19.6156 20.5722 19.9636 20.5054 20.2993C20.4386 20.635 20.2738 20.9434 20.0318 21.1855C19.7897 21.4275 19.4813 21.5924 19.1456 21.6592C18.8098 21.7259 18.4618 21.6917 18.1456 21.5607C17.8293 21.4297 17.559 21.2078 17.3688 20.9232C17.1787 20.6386 17.0771 20.304 17.0771 19.9617V19.7693"
28-
stroke="green"
29-
strokeWidth={strokeWidth}
30-
strokeLinecap="round"
31-
strokeLinejoin="round"
32-
/>
10+
<svg ref={ref} height="1.2rem" width="1.2rem" viewBox="0 0 32 26" fill="none" xmlns="http://www.w3.org/2000/svg">
11+
<g clipPath="url(#clip0_481_602)">
12+
<path
13+
d="M16.3953 21.6621L18.0485 23.3153C18.4869 23.7537 19.0815 24 19.7015 24C20.3215 24 20.9161 23.7537 21.3545 23.3153L27.9669 16.7029C28.4053 16.2645 28.6516 15.6699 28.6516 15.0499C28.6516 14.4299 28.4053 13.8353 27.9669 13.3969L21.3545 6.78457C20.9161 6.34615 20.3215 6.09985 19.7015 6.09985C19.0815 6.09985 18.4869 6.34615 18.0485 6.78457L11.4362 13.3969C10.9978 13.8353 10.7515 14.4299 10.7515 15.0499C10.7515 15.6699 10.9978 16.2645 11.4362 16.7029L13.0893 18.3561"
14+
stroke={stroke}
15+
strokeWidth={strokeWidth}
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
/>
19+
<path
20+
d="M13.0892 8.43764L11.4361 6.78457C10.9977 6.34615 10.4031 6.09985 9.78305 6.09985C9.16303 6.09985 8.56841 6.34615 8.12999 6.78457L1.51772 13.3969C1.07931 13.8353 0.833008 14.4299 0.833008 15.0499C0.833008 15.6699 1.07931 16.2645 1.51772 16.7029L8.12999 23.3153C8.56841 23.7537 9.16303 24 9.78305 24C10.4031 24 10.9977 23.7537 11.4361 23.3153L18.0484 16.7029C18.4868 16.2645 18.7332 15.6699 18.7332 15.0499C18.7332 14.4299 18.4868 13.8353 18.0484 13.3969L16.3953 11.7438"
21+
stroke={stroke}
22+
strokeWidth={strokeWidth}
23+
strokeLinecap="round"
24+
strokeLinejoin="round"
25+
/>
26+
<rect x="7" y="-0.199951" width="25" height="10.5" rx="3" fill="#2DD4BF" />
27+
<path
28+
d="M11.5503 5.49194H9.90674L9.89795 4.58667H11.2778C11.521 4.58667 11.7202 4.55591 11.8755 4.49438C12.0308 4.42993 12.1465 4.33765 12.2227 4.21753C12.3018 4.09448 12.3413 3.94507 12.3413 3.76929C12.3413 3.57007 12.3032 3.40894 12.2271 3.28589C12.1538 3.16284 12.0381 3.07349 11.8799 3.01782C11.7246 2.96216 11.5239 2.93433 11.2778 2.93433H10.3638V8.30005H9.04541V1.90161H11.2778C11.6499 1.90161 11.9824 1.93677 12.2754 2.00708C12.5713 2.07739 12.8218 2.18433 13.0269 2.32788C13.2319 2.47144 13.3887 2.65308 13.4971 2.8728C13.6055 3.0896 13.6597 3.34741 13.6597 3.64624C13.6597 3.90991 13.5996 4.15308 13.4795 4.37573C13.3623 4.59839 13.1763 4.78003 12.9214 4.92065C12.6694 5.06128 12.3398 5.13892 11.9326 5.15356L11.5503 5.49194ZM11.4932 8.30005H9.54639L10.0605 7.27173H11.4932C11.7246 7.27173 11.9136 7.23511 12.0601 7.16187C12.2065 7.08569 12.3149 6.98315 12.3853 6.85425C12.4556 6.72534 12.4907 6.57739 12.4907 6.4104C12.4907 6.2229 12.4585 6.0603 12.394 5.92261C12.3325 5.78491 12.2329 5.67944 12.0952 5.6062C11.9575 5.53003 11.7759 5.49194 11.5503 5.49194H10.2803L10.2891 4.58667H11.8711L12.1743 4.94263C12.564 4.93677 12.8774 5.00562 13.1147 5.14917C13.355 5.28979 13.5293 5.4729 13.6377 5.69849C13.749 5.92407 13.8047 6.16577 13.8047 6.42358C13.8047 6.83374 13.7153 7.17944 13.5366 7.46069C13.3579 7.73901 13.0957 7.94849 12.75 8.08911C12.4072 8.22974 11.9883 8.30005 11.4932 8.30005ZM19.0869 7.27173V8.30005H15.6812V7.27173H19.0869ZM16.1118 1.90161V8.30005H14.7935V1.90161H16.1118ZM18.6431 4.50757V5.50952H15.6812V4.50757H18.6431ZM19.0825 1.90161V2.93433H15.6812V1.90161H19.0825ZM22.8047 1.90161V8.30005H21.4907V1.90161H22.8047ZM24.7734 1.90161V2.93433H19.5527V1.90161H24.7734ZM27.6519 2.99585L25.9116 8.30005H24.5098L26.8872 1.90161H27.7793L27.6519 2.99585ZM29.0977 8.30005L27.353 2.99585L27.2124 1.90161H28.1133L30.5039 8.30005H29.0977ZM29.0186 5.91821V6.95093H25.6392V5.91821H29.0186Z"
29+
fill="black"
30+
/>
31+
</g>
3332
</svg>
34-
35-
// <svg ref={ref} height="1.2rem" width="1.2rem" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
36-
// <path
37-
// d="M13.3333 17.3333L14.6667 18.6667C15.0203 19.0203 15.4999 19.219 16 19.219C16.5001 19.219 16.9797 19.0203 17.3333 18.6667L22.6667 13.3333C23.0203 12.9797 23.219 12.5001 23.219 12C23.219 11.4999 23.0203 11.0203 22.6667 10.6667L17.3333 5.33333C16.9797 4.97971 16.5001 4.78105 16 4.78105C15.4999 4.78105 15.0203 4.97971 14.6667 5.33333L9.33333 10.6667C8.97971 11.0203 8.78105 11.4999 8.78105 12C8.78105 12.5001 8.97971 12.9797 9.33333 13.3333L10.6667 14.6667"
38-
// stroke={stroke}
39-
// strokeWidth={strokeWidth}
40-
// strokeLinecap="round"
41-
// strokeLinejoin="round"
42-
// />
43-
// <path
44-
// d="M10.6667 6.66667L9.33333 5.33333C8.97971 4.97971 8.5001 4.78105 8 4.78105C7.4999 4.78105 7.02029 4.97971 6.66667 5.33333L1.33333 10.6667C0.979711 11.0203 0.781049 11.4999 0.781049 12C0.781049 12.5001 0.979711 12.9797 1.33333 13.3333L6.66667 18.6667C7.02029 19.0203 7.4999 19.219 8 19.219C8.5001 19.219 8.97971 19.0203 9.33333 18.6667L14.6667 13.3333C15.0203 12.9797 15.219 12.5001 15.219 12C15.219 11.4999 15.0203 11.0203 14.6667 10.6667L13.3333 9.33333"
45-
// stroke={stroke}
46-
// strokeWidth={strokeWidth}
47-
// strokeLinecap="round"
48-
// strokeLinejoin="round"
49-
// />
50-
// </svg>
5133
));
5234

5335
CorrelationIcon.displayName = 'CorrelationIcon';

src/hooks/useCorrelationQueryLogs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ export const useCorrelationQueryLogs = () => {
6363
if (fields.length > 0 && !correlationCondition) {
6464
return setCorrelationStore((store) => setStreamData(store, currentStream || '', records));
6565
} else if (fields.length > 0 && correlationCondition) {
66+
setCorrelationStore((store) => setIsCorrelatedFlag(store, true));
6667
return setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records));
6768
} else {
6869
notifyError({ message: `${currentStream} doesn't have any fields` });
6970
}
7071
});
71-
setCorrelationStore((store) => setIsCorrelatedFlag(store, true));
7272
},
7373
onError: (data: AxiosError) => {
7474
setLoading(false);

src/pages/Correlation/Views/CorrelationJSONView.tsx

Lines changed: 100 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, Stack } from '@mantine/core';
1+
import { Box, Button, Loader, Stack, TextInput, Text } from '@mantine/core';
22
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
33
import classes from '../../Stream/styles/JSONView.module.css';
44
import EmptyBox from '@/components/Empty';
@@ -10,12 +10,14 @@ import {
1010
} from '@/constants/theme';
1111
import { Log } from '@/@types/parseable/api/query';
1212
import _ from 'lodash';
13-
import { IconCheck, IconCopy, IconDotsVertical } from '@tabler/icons-react';
13+
import { IconCheck, IconCopy, IconDotsVertical, IconSearch } from '@tabler/icons-react';
1414
import { copyTextToClipboard } from '@/utils';
1515
import timeRangeUtils from '@/utils/timeRangeUtils';
16-
import { useCorrelationStore } from '../providers/CorrelationProvider';
16+
import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider';
1717
import { ErrorView, LoadingView } from '@/pages/Stream/Views/Explore/LoadingViews';
1818
import { formatLogTs, isJqSearch } from '@/pages/Stream/providers/LogsProvider';
19+
import jqSearch from '@/utils/jqSearch';
20+
import { useHotkeys } from '@mantine/hooks';
1921

2022
type ContextMenuState = {
2123
visible: boolean;
@@ -24,7 +26,7 @@ type ContextMenuState = {
2426
row: Log | null;
2527
};
2628

27-
// const { setInstantSearchValue, applyInstantSearch, applyJqSearch } = correlationStoreReducers;
29+
const { setInstantSearchValue, applyInstantSearch, applyJqSearch } = correlationStoreReducers;
2830

2931
const Item = (props: { header: string | null; value: string; highlight: boolean }) => {
3032
return (
@@ -171,105 +173,104 @@ const JsonRows = (props: { isSearching: boolean; setContextMenu: any }) => {
171173
);
172174
};
173175

174-
// const Toolbar = ({
175-
// isSearching,
176-
// setSearching,
177-
// }: {
178-
// isSearching: boolean;
179-
// setSearching: React.Dispatch<React.SetStateAction<boolean>>;
180-
// }) => {
181-
// const [localSearchValue, setLocalSearchValue] = useState<string>('');
182-
// const searchInputRef = useRef<HTMLInputElement>(null);
183-
184-
// const [searchValue, setCorrelationData] = useCorrelationStore((store) => store.tableOpts.instantSearchValue);
185-
// const [{ rawData, filteredData }] = useCorrelationStore((store) => store.data);
176+
const Toolbar = ({
177+
isSearching,
178+
setSearching,
179+
}: {
180+
isSearching: boolean;
181+
setSearching: React.Dispatch<React.SetStateAction<boolean>>;
182+
}) => {
183+
const [localSearchValue, setLocalSearchValue] = useState<string>('');
184+
const searchInputRef = useRef<HTMLInputElement>(null);
186185

187-
// const debouncedSearch = useCallback(
188-
// _.debounce(async (val: string) => {
189-
// if (val.trim() === '') {
190-
// setCorrelationData((store) => setInstantSearchValue(store, ''));
191-
// setCorrelationData(applyInstantSearch);
192-
// } else {
193-
// const isJq = isJqSearch(val);
194-
// if (isJq) {
195-
// const jqResult = await jqSearch(rawData, val);
196-
// setCorrelationData((store) => applyJqSearch(store, jqResult));
197-
// } else {
198-
// setCorrelationData(applyInstantSearch);
199-
// }
200-
// }
201-
// setSearching(false);
202-
// }, 500),
203-
// [rawData],
204-
// );
186+
const [searchValue, setCorrelationData] = useCorrelationStore((store) => store.tableOpts.instantSearchValue);
187+
const [pageData] = useCorrelationStore((store) => store.tableOpts);
188+
const debouncedSearch = useCallback(
189+
_.debounce(async (val: string) => {
190+
if (val.trim() === '') {
191+
setCorrelationData((store) => setInstantSearchValue(store, ''));
192+
setCorrelationData(applyInstantSearch);
193+
} else {
194+
const isJq = isJqSearch(val);
195+
if (isJq) {
196+
const jqResult = await jqSearch(pageData, val);
197+
setCorrelationData((store) => applyJqSearch(store, jqResult));
198+
} else {
199+
setCorrelationData(applyInstantSearch);
200+
}
201+
}
202+
setSearching(false);
203+
}, 500),
204+
[pageData],
205+
);
205206

206-
// const handleSearch = useCallback(() => {
207-
// if (localSearchValue.trim()) {
208-
// setSearching(true);
209-
// setCorrelationData((store) => setInstantSearchValue(store, localSearchValue));
210-
// debouncedSearch(localSearchValue);
211-
// }
212-
// }, [localSearchValue, debouncedSearch, setSearching]);
207+
const handleSearch = useCallback(() => {
208+
if (localSearchValue.trim()) {
209+
setSearching(true);
210+
setCorrelationData((store) => setInstantSearchValue(store, localSearchValue));
211+
debouncedSearch(localSearchValue);
212+
}
213+
}, [localSearchValue, debouncedSearch, setSearching]);
213214

214-
// const handleInputChange = useCallback(
215-
// (e: React.ChangeEvent<HTMLInputElement>) => {
216-
// const value = e.target.value;
217-
// setLocalSearchValue(value);
218-
// if (value.trim() === '') {
219-
// debouncedSearch(value);
220-
// }
221-
// },
222-
// [debouncedSearch],
223-
// );
215+
const handleInputChange = useCallback(
216+
(e: React.ChangeEvent<HTMLInputElement>) => {
217+
const value = e.target.value;
218+
setLocalSearchValue(value);
219+
if (value.trim() === '') {
220+
debouncedSearch(value);
221+
}
222+
},
223+
[debouncedSearch],
224+
);
224225

225-
// useHotkeys([['mod+K', () => searchInputRef.current?.focus()]]);
226+
useHotkeys([['mod+K', () => searchInputRef.current?.focus()]]);
226227

227-
// const handleKeyDown = useCallback(
228-
// (e: React.KeyboardEvent<HTMLInputElement>) => {
229-
// if (e.key === 'Enter' && !isSearching && localSearchValue.trim()) {
230-
// handleSearch();
231-
// }
232-
// },
233-
// [isSearching, localSearchValue],
234-
// );
228+
const handleKeyDown = useCallback(
229+
(e: React.KeyboardEvent<HTMLInputElement>) => {
230+
if (e.key === 'Enter' && !isSearching && localSearchValue.trim()) {
231+
handleSearch();
232+
}
233+
},
234+
[isSearching, localSearchValue],
235+
);
235236

236-
// if (_.isEmpty(rawData)) return null;
237+
if (_.isEmpty(pageData)) return null;
237238

238-
// const inputStyles = {
239-
// '--input-left-section-width': '2rem',
240-
// '--input-right-section-width': '6rem',
241-
// width: '100%',
242-
// } as React.CSSProperties;
239+
const inputStyles = {
240+
'--input-left-section-width': '2rem',
241+
'--input-right-section-width': '6rem',
242+
width: '100%',
243+
} as React.CSSProperties;
243244

244-
// return (
245-
// <div className={classes.headerWrapper}>
246-
// <TextInput
247-
// leftSection={isSearching ? <Loader size="sm" /> : <IconSearch stroke={2.5} size="0.9rem" />}
248-
// placeholder="Search loaded data with text or jq. For jq input try `jq .[]`"
249-
// value={localSearchValue}
250-
// onChange={handleInputChange}
251-
// onKeyDown={handleKeyDown}
252-
// ref={searchInputRef}
253-
// classNames={{ input: classes.inputField }}
254-
// style={inputStyles}
255-
// rightSection={
256-
// searchValue && !isSearching ? (
257-
// <Text style={{ fontSize: '0.7rem', textAlign: 'end' }} lineClamp={1}>
258-
// {filteredData.length} Matches
259-
// </Text>
260-
// ) : null
261-
// }
262-
// />
263-
// <Button
264-
// onClick={handleSearch}
265-
// disabled={!localSearchValue.trim() || isSearching}
266-
// style={{ width: '10%' }}
267-
// leftSection={<IconSearch stroke={2.5} size="0.9rem" />}>
268-
// Search
269-
// </Button>
270-
// </div>
271-
// );
272-
// };
245+
return (
246+
<div className={classes.headerWrapper}>
247+
<TextInput
248+
leftSection={isSearching ? <Loader size="sm" /> : <IconSearch stroke={2.5} size="0.9rem" />}
249+
placeholder="Search loaded data with text or jq. For jq input try `jq .[]`"
250+
value={localSearchValue}
251+
onChange={handleInputChange}
252+
onKeyDown={handleKeyDown}
253+
ref={searchInputRef}
254+
classNames={{ input: classes.inputField }}
255+
style={inputStyles}
256+
rightSection={
257+
searchValue && !isSearching ? (
258+
<Text style={{ fontSize: '0.7rem', textAlign: 'end' }} lineClamp={1}>
259+
Matches
260+
</Text>
261+
) : null
262+
}
263+
/>
264+
<Button
265+
onClick={handleSearch}
266+
disabled={!localSearchValue.trim() || isSearching}
267+
style={{ width: '10%' }}
268+
leftSection={<IconSearch stroke={2.5} size="0.9rem" />}>
269+
Search
270+
</Button>
271+
</div>
272+
);
273+
};
273274

274275
const TableContainer = (props: { children: ReactNode }) => {
275276
return <Box className={classes.container}>{props.children}</Box>;
@@ -286,11 +287,13 @@ const CorrleationJSONView = (props: { errorMessage: string | null; hasNoData: bo
286287

287288
const contextMenuRef = useRef<HTMLDivElement>(null);
288289
const { errorMessage, hasNoData, showTable } = props;
289-
const [isSearching] = useState(false);
290+
const [isSearching, setSearching] = useState(false);
290291
const primaryHeaderHeight = !maximized
291292
? PRIMARY_HEADER_HEIGHT + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT + STREAM_SECONDARY_TOOLBAR_HRIGHT
292293
: 0;
293294

295+
// const showTableOrLoader = logsLoading || streamsLoading || showTable || !errorMessage || !hasNoData;
296+
294297
useEffect(() => {
295298
const handleClickOutside = (event: MouseEvent) => {
296299
if (contextMenuRef.current && !contextMenuRef.current.contains(event.target as Node)) {
@@ -311,7 +314,7 @@ const CorrleationJSONView = (props: { errorMessage: string | null; hasNoData: bo
311314

312315
return (
313316
<TableContainer>
314-
{/* <Toolbar isSearching={isSearching} setSearching={setSearching} /> */}
317+
<Toolbar isSearching={isSearching} setSearching={setSearching} />
315318
{!errorMessage ? (
316319
showTable ? (
317320
<Box className={classes.innerContainer} style={{ maxHeight: `calc(100vh - ${primaryHeaderHeight}px )` }}>

src/pages/Correlation/index.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,6 @@ const Correlation = () => {
167167
}
168168
}, [currentStream, fields]);
169169

170-
useEffect(() => {
171-
getFetchStreamData();
172-
}, [isCorrelatedData]);
173-
174170
useEffect(() => {
175171
if (isCorrelatedData) {
176172
getCorrelationData();
@@ -274,6 +270,8 @@ const Correlation = () => {
274270
const totalStreams = Object.entries(fields).length;
275271
const heightPercentage = totalStreams === 1 ? '50%' : `${100 / totalStreams}%`;
276272

273+
const isLoading = loadingState || schemaLoading || streamsLoading || multipleSchemasLoading;
274+
if (!fieldsIter) return;
277275
return (
278276
<div
279277
key={stream}
@@ -303,7 +301,7 @@ const Correlation = () => {
303301
}}
304302
/>
305303
</div>
306-
{loadingState || schemaLoading || streamsLoading || multipleSchemasLoading ? (
304+
{isLoading ? (
307305
<Stack style={{ padding: '0.5rem 0.7rem' }}>
308306
{Array.from({ length: 8 }).map((_, index) => (
309307
<Skeleton key={index} height="24px" />
@@ -492,7 +490,7 @@ const Correlation = () => {
492490
<SavedCorrelationsButton />
493491
<ViewToggle />
494492

495-
{isCorrelationEnabled && (
493+
{correlationCondition && (
496494
<Stack style={{ flexDirection: 'row', alignItems: 'center' }} gap={8}>
497495
<Pill
498496
withRemoveButton

0 commit comments

Comments
 (0)