Skip to content

Commit

Permalink
[GEN-1462] feat: fast sources selection (#1563)
Browse files Browse the repository at this point in the history
added tree checkboxs selection for large scale clusters
  • Loading branch information
alonkeyval authored Oct 1, 2024
1 parent 80d646b commit 61a0b3b
Show file tree
Hide file tree
Showing 6 changed files with 442 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import { SourcesOptionMenuWrapper } from './sources.option.menu.styled';
import { FilterSourcesOptions } from './filter.sources.options';
import { ActionSourcesOptions } from './action.sources.options';
import { KeyvalButton, KeyvalText } from '@/design.system';
import theme from '@/styles/palette';

export function SourcesOptionMenu({
setCurrentItem,
Expand All @@ -12,6 +14,7 @@ export function SourcesOptionMenu({
selectedApplications,
currentNamespace,
onFutureApplyChange,
toggleFastSourcesSelection,
}: any) {
return (
<SourcesOptionMenuWrapper>
Expand All @@ -28,6 +31,11 @@ export function SourcesOptionMenu({
selectedApplications={selectedApplications}
onFutureApplyChange={onFutureApplyChange}
/>
<KeyvalButton style={{ height: 36 }} onClick={toggleFastSourcesSelection}>
<KeyvalText color={theme.text.dark_button}>
Fast Sources Selection
</KeyvalText>
</KeyvalButton>
</SourcesOptionMenuWrapper>
);
}
216 changes: 216 additions & 0 deletions frontend/webapp/containers/setup/sources/fast-sources-selection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { KeyvalButton, KeyvalLoader, KeyvalText } from '@/design.system';
import { OVERVIEW, QUERIES, ROUTES } from '@/utils';
import { getApplication, getNamespaces } from '@/services';
import {
LoaderWrapper,
SectionContainerWrapper,
} from './sources.section.styled';
import { NamespaceAccordion } from './namespace-accordion';
import styled from 'styled-components';
import theme from '@/styles/palette';
import { useSources } from '@/hooks';
import { useRouter } from 'next/navigation';
import { setSources } from '@/store';
import { useDispatch } from 'react-redux';

const TitleWrapper = styled.div`
margin-bottom: 24px;
`;

const ButtonWrapper = styled.div`
position: absolute;
bottom: 24px;
width: 90%;
`;

interface AccordionItem {
name: string;
kind: string;
instances: number;
app_instrumentation_labeled: boolean;
ns_instrumentation_labeled: boolean;
instrumentation_effective: boolean;
selected: boolean;
}

interface AccordionData {
title: string;
items: AccordionItem[];
}

export function FastSourcesSelection({ sectionData, setSectionData }) {
const [accordionData, setAccordionData] = useState<AccordionData[]>([]);
const router = useRouter();
const dispatch = useDispatch();
const { isLoading, data: namespaces } = useQuery(
[QUERIES.API_NAMESPACES],
getNamespaces
);

const { upsertSources } = useSources();

useEffect(() => {
if (namespaces) {
const accordionData = namespaces.namespaces.map((item, index) => ({
title: item.name,
items:
sectionData?.[item.name]?.objects.map((app) => ({
...app,
selected: app.selected,
})) || [],
}));
setAccordionData(accordionData);
}
}, [namespaces]);

const handleSetCurrentNamespace = async (selectedNs) => {
const currentNsApps = await getApplication(selectedNs.title);

const updatedSectionData = {
...sectionData,
[selectedNs.title]: {
selected_all: sectionData[selectedNs.title]?.selected_all || false,
future_selected:
sectionData[selectedNs.title]?.future_selected || false,
objects: currentNsApps.applications.map((app) => ({
...app,
selected: sectionData[selectedNs.title]?.objects?.find(
(a) => a.name === app.name
)?.selected,
})),
},
};

const accordionData = namespaces.namespaces.map((item) => ({
title: item.name,
items: updatedSectionData[item.name]?.objects
.map((app) => ({
...app,
selected: app.selected,
}))
.filter((app) => !app.instrumentation_effective),
}));

// Update both sectionData and accordionData
setSectionData(updatedSectionData);
setAccordionData(accordionData);
};

const onSelectItemChange = (item: AccordionItem, ns: string) => {
const updatedAccordionData = accordionData.map((a_data: AccordionData) => {
if (a_data.title === ns) {
return {
...a_data,
items: a_data.items.map((i: AccordionItem) => {
if (i.name === item.name) {
return { ...i, selected: !i.selected };
}
return i;
}),
};
}
return a_data;
});

const updatedSectionData = {
...sectionData,
[ns]: {
...sectionData[ns],
selected_all: accordionData[ns]
?.find((a) => a.title === ns)
.items.every((i) => i.selected),
future_selected: accordionData[ns]
?.find((a) => a.title === ns)
.items.some((i) => !i.selected),
objects: sectionData[ns].objects.map((a) => {
if (a.name === item.name) {
return { ...a, selected: !a.selected };
}
return a;
}),
},
};

setSectionData(updatedSectionData);

// Update the accordion data state with the modified data
setAccordionData(updatedAccordionData);
};

const onSelectAllChange = (ns, value) => {
const updatedAccordionData = accordionData.map((a_data: AccordionData) => {
if (a_data.title === ns) {
return {
...a_data,
items: a_data.items.map((i: AccordionItem) => {
return { ...i, selected: value };
}),
};
}
return a_data;
});

const updatedSectionData = {
...sectionData,
[ns]: {
...sectionData[ns],
objects: sectionData[ns]?.objects?.map((a) => {
return { ...a, selected: value };
}),
},
};

// Update the accordion data state with the modified data
setSectionData(updatedSectionData);
setAccordionData(updatedAccordionData);
};

function onConnectClick() {
const isSetup = window.location.pathname.includes('choose-sources');

if (isSetup) {
dispatch(setSources(sectionData));
router.push(ROUTES.CHOOSE_DESTINATION);
return;
}

upsertSources({
sectionData,
onSuccess: () => router.push(`${ROUTES.SOURCES}?poll=true`),
onError: null,
});
}

if (isLoading) {
return (
<LoaderWrapper>
<KeyvalLoader />
</LoaderWrapper>
);
}
const isSetup = window.location.pathname.includes('choose-sources');
return (
<SectionContainerWrapper>
<TitleWrapper>
<KeyvalText>Fast Sources Selection</KeyvalText>
</TitleWrapper>
<div style={{ height: '75vh' }}>
<NamespaceAccordion
data={accordionData}
onSelectItem={onSelectItemChange}
setCurrentNamespace={(data) => handleSetCurrentNamespace(data)}
onSelectAllChange={onSelectAllChange}
/>
</div>
<ButtonWrapper>
<KeyvalButton onClick={onConnectClick}>
<KeyvalText weight={600} color={theme.text.dark_button}>
{isSetup ? 'Next' : OVERVIEW.CONNECT}
</KeyvalText>
</KeyvalButton>
</ButtonWrapper>
</SectionContainerWrapper>
);
}
125 changes: 125 additions & 0 deletions frontend/webapp/containers/setup/sources/namespace-accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { KeyvalCheckbox, KeyvalText } from '@/design.system';
import React, { useEffect, useState } from 'react';
import { WhiteArrowIcon } from '@keyval-dev/design-system';
import styled from 'styled-components';
interface AccordionItemData {
title: string;
items: any[];
}

interface AccordionProps {
data: AccordionItemData[];
setCurrentNamespace: (data: AccordionItemData) => void;
onSelectItem: (item: any, ns: string) => void;
onSelectAllChange: (ns, value) => void;
}

const ArrowIconWrapper = styled.div<{ expanded: boolean }>`
margin-left: 24px;
transform: rotate(${({ expanded }) => (expanded ? '90deg' : '-90deg')});
`;

export function NamespaceAccordion({
data,
setCurrentNamespace,
onSelectItem,
onSelectAllChange,
}: AccordionProps) {
return (
<div>
{data.map((itemData, index) => (
<AccordionItem
key={index}
data={itemData}
onSelectItem={onSelectItem}
setCurrentNamespace={setCurrentNamespace}
onSelectAllChange={onSelectAllChange}
/>
))}
</div>
);
}

interface AccordionItemProps {
data: any;
setCurrentNamespace: (data: AccordionItemData) => void;
onSelectItem: (item: any, ns: string) => void;
onSelectAllChange: (ns, value) => void;
}

function AccordionItem({
data,
setCurrentNamespace,
onSelectItem,
onSelectAllChange,
}: AccordionItemProps) {
const [isAllSelected, setIsAllSelected] = useState(false);
const [expanded, setExpanded] = useState(false);

useEffect(() => {
const selectedItems = data.items?.filter((item) => item.selected);
setIsAllSelected(
selectedItems?.length === data?.items?.length && selectedItems?.length > 0
);
}, [data]);

const handleSelectAllChange = (ns, value) => {
onSelectAllChange(ns, value);
setIsAllSelected(value);
};

const handleItemChange = (item) => {
onSelectItem(item, data.title);
};

const handleExpand = () => {
setCurrentNamespace(data);
setExpanded(!expanded);
};

return (
<div>
<div
style={{
marginBottom: 8,
display: 'flex',
alignItems: 'center',
}}
>
<KeyvalCheckbox
value={isAllSelected}
onChange={() => handleSelectAllChange(data.title, !isAllSelected)}
label={''}
/>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
}}
onClick={handleExpand}
>
<KeyvalText style={{ marginLeft: 8, flex: 1, cursor: 'pointer' }}>
{data.title}
</KeyvalText>
<ArrowIconWrapper expanded={expanded}>
<WhiteArrowIcon size={10} />
</ArrowIconWrapper>
</div>
</div>
{expanded && (
<div style={{ paddingLeft: '20px' }}>
{data.items?.map((item, index) => (
<div key={index} style={{ cursor: 'pointer', marginBottom: 8 }}>
<KeyvalCheckbox
value={item.selected}
onChange={() => handleItemChange(item)}
label={`${item.name} / ${item.kind.toLowerCase()}`}
/>
</div>
))}
</div>
)}
</div>
);
}
Loading

0 comments on commit 61a0b3b

Please sign in to comment.