Skip to content

Commit 35f04e1

Browse files
committed
In devfile search view, add an option to filter by debug support and deploy support redhat-developer#3086
Fixes: redhat-developer#3086 Signed-off-by: Victor Rubezhny <[email protected]>
1 parent a29e35a commit 35f04e1

File tree

4 files changed

+354
-42
lines changed

4 files changed

+354
-42
lines changed

src/webview/common-ext/createComponentHelpers.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,36 @@ export function getDevfileRegistries(): DevfileRegistry[] {
169169
devfileRegistries.sort((a, b) => (a.name < b.name ? -1 : 1));
170170
return devfileRegistries;
171171
}
172+
173+
/**
174+
* Returns a list of possible the devfile capabilities.
175+
*
176+
* Currently the capabilities are predefined and include:
177+
* - Debug Support
178+
* - Deploy Support
179+
*
180+
* @returns a list of the devfile capabilities
181+
*/
182+
export function getDevfileCapabilities(): string[] {
183+
return [ 'Debug Support', 'Deploy Support'];
184+
}
185+
186+
/**
187+
* Returns a list of the devfile tags found in devfiles registries.
188+
*
189+
* @returns a list of the devfile tags
190+
*/
191+
export function getDevfileTags(url?:string ): string[] {
192+
const devfileRegistries = getDevfileRegistries();
193+
194+
const devfileTags: string[] = [
195+
...new Set(
196+
devfileRegistries
197+
.filter((devfileRegistry) => url ? devfileRegistry.url === url : true)
198+
.flatMap((_devfileRegistry) => _devfileRegistry.devfiles)
199+
.flatMap((_devfile) => _devfile.tags))
200+
]
201+
.sort((a, b) => a.localeCompare(b));
202+
203+
return devfileTags;
204+
}

src/webview/common/devfileSearch.tsx

Lines changed: 238 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*-----------------------------------------------------------------------------------------------*/
55
import { Close, FileCopy, Launch, Search } from '@mui/icons-material';
66
import {
7-
Box, Button,
7+
Box,
8+
Button,
9+
Chip,
810
Checkbox,
911
Divider,
1012
FormControl,
@@ -135,31 +137,105 @@ function RegistriesPicker(props: {
135137
}
136138

137139
return (
138-
<Stack direction="column" spacing={1} marginY={2}>
139-
<Typography variant="body2" marginBottom={1}>
140-
Devfile Registries
141-
</Typography>
142-
<FormGroup>
143-
{props.registryEnabled.map((registry) => {
144-
return (
145-
<FormControlLabel
146-
control={
147-
<Checkbox
148-
disabled={
149-
registry.registryUrl === 'https://registry.devfile.io'
150-
}
151-
checked={registry.enabled}
152-
onChange={(_e, checked) =>
153-
onCheckboxClick(registry.registryName, checked)
154-
}
155-
/>
156-
}
157-
label={registry.registryName}
158-
key={registry.registryName}
159-
/>
160-
);
161-
})}
162-
</FormGroup>
140+
<FormGroup>
141+
{props.registryEnabled.map((registry) => {
142+
return (
143+
<FormControlLabel
144+
control={
145+
<Checkbox
146+
size='small'
147+
disabled={
148+
registry.registryUrl === 'https://registry.devfile.io'
149+
}
150+
checked={registry.enabled}
151+
onChange={(_e, checked) =>
152+
onCheckboxClick(registry.registryName, checked)
153+
}
154+
/>
155+
}
156+
label={registry.registryName}
157+
key={registry.registryName}
158+
/>
159+
);
160+
})}
161+
</FormGroup>
162+
);
163+
}
164+
165+
function RegistryCapabilitiesPicker(props: {
166+
capabilityEnabled: { capabilityName: string; enabled: boolean }[];
167+
setCapabilityEnabled: React.Dispatch<
168+
React.SetStateAction<{ capabilityName: string; enabled: boolean }[]>
169+
>;
170+
}) {
171+
function onClick(clickedCapability: string, checked: boolean) {
172+
const updatedList = [...props.capabilityEnabled] //
173+
.filter((entry) => entry.capabilityName !== clickedCapability);
174+
updatedList.push({
175+
capabilityName: clickedCapability,
176+
enabled: checked,
177+
});
178+
const filteredUpdatedList = updatedList
179+
.sort((capA, capB) => {
180+
return capA.capabilityName.localeCompare(capB.capabilityName)
181+
});
182+
props.setCapabilityEnabled([...filteredUpdatedList]);
183+
}
184+
185+
return (
186+
<Stack spacing={1} useFlexGap direction='row' flexWrap='wrap'>
187+
{props.capabilityEnabled.map((_cap) => {
188+
return (
189+
<Chip
190+
size="small"
191+
sx={{ borderSpacing: '3', margin:'1' }}
192+
clickable={true}
193+
color={_cap.enabled ? 'success':'default'}
194+
onClick={(_) => {onClick(_cap.capabilityName, !_cap.enabled)}}
195+
label={_cap.capabilityName}
196+
key={_cap.capabilityName}
197+
/>
198+
);
199+
})}
200+
</Stack>
201+
);
202+
}
203+
204+
function RegistryTagsPicker(props: {
205+
tagEnabled: { tagName: string; enabled: boolean }[];
206+
setTagEnabled: React.Dispatch<
207+
React.SetStateAction<{ tagName: string; enabled: boolean }[]>
208+
>;
209+
}) {
210+
function onClick(clickedTag: string, checked: boolean) {
211+
const updatedList = [...props.tagEnabled] //
212+
.filter((entry) => entry.tagName !== clickedTag);
213+
updatedList.push({
214+
tagName: clickedTag,
215+
enabled: checked,
216+
});
217+
const filteredUpdatedList = updatedList
218+
.sort((tagA, tagB) => {
219+
return tagA.tagName.localeCompare(tagB.tagName)
220+
});
221+
props.setTagEnabled([...filteredUpdatedList]);
222+
}
223+
224+
return (
225+
<Stack spacing={1} useFlexGap direction='row' flexWrap='wrap'>
226+
{props.tagEnabled.map((_tag) => {
227+
return (
228+
<Chip
229+
size="small"
230+
sx={{ borderSpacing: '3', margin:'1' }}
231+
clickable={true}
232+
color={_tag.enabled ? 'success':'default'}
233+
onClick={(_) => {onClick(_tag.tagName, !_tag.enabled)}}
234+
label={_tag.tagName}
235+
key={_tag.tagName}
236+
/>
237+
);
238+
})}
163239
</Stack>
164240
);
165241
}
@@ -391,6 +467,26 @@ export type DevfileSearchProps = {
391467
goBack?: () => void;
392468
};
393469

470+
/**
471+
* Calculates if specified devfile is to be included into search results
472+
* based on devfile tags and tags filter. A devfile is to be included if:
473+
* - it contains any of selected tags
474+
* - always if there is no any selected tags
475+
*
476+
* @param tags1
477+
* @param tags2
478+
* @returns
479+
*/
480+
function isToBeIncluded(devfile: Devfile, tagFilter: string[], debugSupportFilter: boolean, deploySupportFilter: boolean): boolean {
481+
const includesDebugSupport = debugSupportFilter === false || debugSupportFilter === devfile.supportsDebug;
482+
const includesDeploySupport = deploySupportFilter === false || deploySupportFilter === devfile.supportsDeploy;
483+
const includesTags = tagFilter.length === 0 || devfile.tags.filter((_devfileTag) => {
484+
return tagFilter.find((_selectedTags) => _devfileTag === _selectedTags) !== undefined;
485+
}).length > 0;
486+
487+
return includesDebugSupport && includesDeploySupport && includesTags;
488+
}
489+
394490
export function DevfileSearch(props: DevfileSearchProps) {
395491
const ITEMS_PER_PAGE = 6;
396492
const QUARKUS_REGEX = /[Qq]uarkus/;
@@ -401,6 +497,14 @@ export function DevfileSearch(props: DevfileSearchProps) {
401497
const [registryEnabled, setRegistryEnabled] = React.useState<
402498
{ registryName: string; registryUrl: string; enabled: boolean }[]
403499
>([]);
500+
const [devfileCapabilities, setDevfileCapabilities] = React.useState<string[]>([]);
501+
const [capabilityEnabled, setCapabilityEnabled] = React.useState<
502+
{ capabilityName: string; enabled: boolean }[]
503+
>([]);
504+
const [devfileTags, setDevfileTags] = React.useState<string[]>([]);
505+
const [tagEnabled, setTagEnabled] = React.useState<
506+
{ tagName: string; enabled: boolean }[]
507+
>([]);
404508
const [searchText, setSearchText] = React.useState('');
405509

406510
function respondToMessage(messageEvent: MessageEvent) {
@@ -410,6 +514,14 @@ export function DevfileSearch(props: DevfileSearchProps) {
410514
setDevfileRegistries((_devfileRegistries) => message.data);
411515
break;
412516
}
517+
case 'devfileCapabilities': {
518+
setDevfileCapabilities((_devfileCapabilities) => message.data);
519+
break;
520+
}
521+
case 'devfileTags': {
522+
setDevfileTags((_devfileTags) => message.data);
523+
break;
524+
}
413525
default:
414526
break;
415527
}
@@ -427,6 +539,28 @@ export function DevfileSearch(props: DevfileSearchProps) {
427539
setRegistryEnabled((_) => enabledArray);
428540
}, [devfileRegistries]);
429541

542+
React.useEffect(() => {
543+
const enabledArray = [];
544+
for (const capability of devfileCapabilities) {
545+
enabledArray.push({
546+
capabilityName: capability,
547+
enabled: false, // All values set to false means that no filter is to be applied
548+
});
549+
}
550+
setCapabilityEnabled((_) => enabledArray);
551+
}, [devfileCapabilities]);
552+
553+
React.useEffect(() => {
554+
const enabledArray = [];
555+
for (const tag of devfileTags) {
556+
enabledArray.push({
557+
tagName: tag,
558+
enabled: false, // All values set to false means that no filter is to be applied
559+
});
560+
}
561+
setTagEnabled((_) => enabledArray);
562+
}, [devfileTags]);
563+
430564
React.useEffect(() => {
431565
props.setSelectedDevfile(selectedDevfile);
432566
}, [selectedDevfile]);
@@ -442,21 +576,52 @@ export function DevfileSearch(props: DevfileSearchProps) {
442576
window.vscodeApi.postMessage({ action: 'getDevfileRegistries' });
443577
}, []);
444578

579+
React.useEffect(() => {
580+
window.vscodeApi.postMessage({ action: 'getDevfileCapabilities' });
581+
}, []);
582+
583+
React.useEffect(() => {
584+
window.vscodeApi.postMessage({ action: 'getDevfileTags' });
585+
}, []);
586+
445587
React.useEffect(() => {
446588
setCurrentPage((_) => 1);
447-
}, [registryEnabled, searchText]);
589+
}, [registryEnabled, capabilityEnabled, tagEnabled, searchText]);
448590

449591
if (!devfileRegistries) {
450592
return <LoadScreen title="Retrieving list of Devfiles" />;
451593
}
452594

595+
if(!devfileCapabilities) {
596+
return <LoadScreen title="Retrieving list of Devfile Capabilities" />;
597+
}
598+
599+
if(!devfileTags) {
600+
return <LoadScreen title="Retrieving list of Devfile Tags" />;
601+
}
602+
453603
const activeRegistries = registryEnabled //
454604
.filter((entry) => entry.enabled) //
455605
.map((entry) => entry.registryName);
456606

607+
const debugSupport = capabilityEnabled //
608+
.filter((_cap) => _cap.capabilityName === 'Debug Support') //
609+
.filter((_cap) => _cap.enabled) //
610+
.length > 0;
611+
612+
const deploySupport = capabilityEnabled //
613+
.filter((_cap) => _cap.capabilityName === 'Deploy Support') //
614+
.filter((_cap) => _cap.enabled) //
615+
.length > 0;
616+
617+
const activeTags = tagEnabled
618+
.filter((_tag) => _tag.enabled) //
619+
.map((_tag) => _tag.tagName);
620+
457621
const devfiles: Devfile[] = devfileRegistries //
458622
.filter((devfileRegistry) => activeRegistries.includes(devfileRegistry.name)) //
459623
.flatMap((devfileRegistry) => devfileRegistry.devfiles) //
624+
.filter((devfile) => isToBeIncluded(devfile, activeTags, debugSupport, deploySupport)) //
460625
.filter((devfile) => {
461626
const searchTerms = searchText.split(/\s+/);
462627
return every(
@@ -490,17 +655,52 @@ export function DevfileSearch(props: DevfileSearchProps) {
490655
<>
491656
<Stack direction="column" height="100%" spacing={3}>
492657
<Typography variant="h5">{props.titleText}</Typography>
493-
<Stack direction="row" flexGrow="1" spacing={2}>
494-
{devfileRegistries.length > 1 && (
495-
<>
496-
<RegistriesPicker
497-
registryEnabled={registryEnabled}
498-
setRegistryEnabled={setRegistryEnabled}
499-
/>
500-
<Divider orientation="vertical" />
501-
</>
502-
)}
503-
<Stack direction="column" sx={{ flexGrow: '1', height: '100%' }} spacing={3}>
658+
<Stack direction="row" spacing={2}>
659+
<Stack direction="column" sx={{ height: 'calc(100vh - 200px - 5em)', overflow: 'scroll', maxWidth:'30%' }} spacing={0}>
660+
{devfileRegistries.length > 1 && (
661+
<>
662+
<Typography variant="body2" marginBottom={1}>
663+
Devfile Registries
664+
</Typography>
665+
<Stack direction="column" sx={{width: '100%' }} width="100%" spacing={0} marginBottom={3}>
666+
<RegistriesPicker
667+
registryEnabled={registryEnabled}
668+
setRegistryEnabled={setRegistryEnabled}
669+
/>
670+
</Stack>
671+
</>
672+
)}
673+
{(devfileCapabilities.length > 0 || devfileTags.length > 0) && (
674+
<>
675+
<Typography variant="body2" marginBottom={2}>
676+
Filter by
677+
</Typography>
678+
<Stack direction="column" useFlexGap={true} width="100%" spacing={1}>
679+
{devfileCapabilities.length > 0 && (
680+
<>
681+
<RegistryCapabilitiesPicker
682+
capabilityEnabled={capabilityEnabled}
683+
setCapabilityEnabled={setCapabilityEnabled}
684+
/>
685+
<Divider orientation="horizontal" sx={{width: '100%' }} />
686+
</>
687+
)}
688+
{devfileTags.length > 0 && (
689+
<RegistryTagsPicker
690+
tagEnabled={tagEnabled}
691+
setTagEnabled={setTagEnabled}
692+
/>
693+
)}
694+
</Stack>
695+
</>
696+
)}
697+
<Stack direction="column" sx={{ flexGrow: '1', height: '100%', width: '100%' }} spacing={0}>
698+
</Stack>
699+
</Stack>
700+
<Stack direction="column" spacing={3}>
701+
<Divider orientation="vertical" sx={{ height: 'calc(100vh - 200px - 5em)'}} />
702+
</Stack>
703+
<Stack direction="column" sx={{ flexGrow: '1' }} spacing={3}>
504704
<SearchBar
505705
searchText={searchText}
506706
setSearchText={setSearchText}

0 commit comments

Comments
 (0)