Skip to content

Commit

Permalink
[GEN-1787]: descriptive titles for drawer "details card" (#1872)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenElferink authored Nov 27, 2024
1 parent 21b5af8 commit 480dcb2
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const ActionDrawer: React.FC<Props> = () => {
/>
</FormContainer>
) : (
<CardDetails data={cardData} />
<CardDetails title='Action Details' data={cardData} />
)}
</OverviewDrawer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React, { useMemo, useState } from 'react';
import { ACTION } from '@/utils';
import buildCard from './build-card';
import styled from 'styled-components';
import { safeJsonParse } from '@/utils';
import { useDrawerStore } from '@/store';
import { CardDetails } from '@/components';
import buildDrawerItem from './build-drawer-item';
import { ConditionDetails } from '@/reuseable-components';
import type { ActualDestination } from '@/types';
import OverviewDrawer from '../../overview/overview-drawer';
import { DestinationFormBody } from '../destination-form-body';
import { OVERVIEW_ENTITY_TYPES, type ActualDestination } from '@/types';
import { useDestinationCRUD, useDestinationFormData, useDestinationTypes } from '@/hooks';

interface Props {}
Expand All @@ -21,94 +18,96 @@ const FormContainer = styled.div`
overflow-y: auto;
`;

const DataContainer = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`;

export const DestinationDrawer: React.FC<Props> = () => {
const { selectedItem, setSelectedItem } = useDrawerStore();
const { destinations: destinationTypes } = useDestinationTypes();
const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem);
const [isEditing, setIsEditing] = useState(false);
const [isFormDirty, setIsFormDirty] = useState(false);

const { updateDestination, deleteDestination } = useDestinationCRUD();
const { formData, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem, destinationTypeDetails, dynamicFields, setDynamicFields } = useDestinationFormData({
destinationType: (selectedItem?.item as ActualDestination)?.destinationType?.type,
preLoadedFields: (selectedItem?.item as ActualDestination)?.fields,
// TODO: supportedSignals: thisDestination?.supportedSignals,
// currently, the real "supportedSignals" is being used by "destination" passed as prop to "DestinationFormBody"
});

const { updateDestination, deleteDestination } = useDestinationCRUD({
onSuccess: (type) => {
setIsEditing(false);
setIsFormDirty(false);

if (type === ACTION.DELETE) {
setSelectedItem(null);
} else {
const { item } = selectedItem as { item: ActualDestination };
const { id } = item;
setSelectedItem({ id, type: OVERVIEW_ENTITY_TYPES.DESTINATION, item: buildDrawerItem(id, formData, item) });
}
},
});
const cardData = useMemo(() => {
if (!selectedItem) return [];

const [isEditing, setIsEditing] = useState(false);
const [isFormDirty, setIsFormDirty] = useState(false);
const buildMonitorsList = (exportedSignals: ActualDestination['exportedSignals']): string =>
Object.keys(exportedSignals)
.filter((key) => exportedSignals[key])
.join(', ') || 'N/A';

const cardData = useMemo(() => {
if (!selectedItem || !destinationTypeDetails) return [];
const buildDestinationFieldData = (parsedFields: Record<string, string>) =>
Object.entries(parsedFields).map(([key, value]) => {
const found = destinationTypeDetails?.fields?.find((field) => field.name === key);

const { item } = selectedItem as { item: ActualDestination };
const arr = buildCard(item, destinationTypeDetails);
const { type } = safeJsonParse(found?.componentProperties, { type: '' });
const secret = type === 'password' ? new Array(value.length).fill('•').join('') : '';

return {
title: found?.displayName || key,
value: secret || value || 'N/A',
};
});

return arr;
const { exportedSignals, destinationType, fields } = selectedItem.item as ActualDestination;
const parsedFields = safeJsonParse<Record<string, string>>(fields, {});
const fieldsData = buildDestinationFieldData(parsedFields);

return [{ title: 'Destination', value: destinationType.displayName || 'N/A' }, { title: 'Monitors', value: buildMonitorsList(exportedSignals) }, ...fieldsData];
}, [selectedItem, destinationTypeDetails]);

const { destinations } = useDestinationTypes();
const thisDestination = useMemo(() => {
if (!destinationTypes.length || !selectedItem || !isEditing) {
if (!destinations.length || !selectedItem || !isEditing) {
resetFormData();
return undefined;
}

const { item } = selectedItem as { item: ActualDestination };
const found = destinationTypes.map(({ items }) => items.filter(({ type }) => type === item.destinationType.type)).filter((arr) => !!arr.length)[0][0];
const found = destinations.map(({ items }) => items.filter(({ type }) => type === item.destinationType.type)).filter((arr) => !!arr.length)[0][0];

if (!found) return undefined;

loadFormWithDrawerItem(selectedItem);

return found;
}, [destinationTypes, selectedItem, isEditing]);
}, [destinations, selectedItem, isEditing]);

if (!selectedItem?.item) return null;
const { id, item } = selectedItem as { id: string; item: ActualDestination };
const { id, item } = selectedItem;

const handleEdit = (bool?: boolean) => {
setIsEditing(typeof bool === 'boolean' ? bool : true);
if (typeof bool === 'boolean') {
setIsEditing(bool);
} else {
setIsEditing(true);
}
};

const handleCancel = () => {
resetFormData();
setIsEditing(false);
setIsFormDirty(false);
};

const handleDelete = async () => {
await deleteDestination(id);
await deleteDestination(id as string);
};

const handleSave = async (newTitle: string) => {
if (validateForm({ withAlert: true })) {
const title = newTitle !== item.destinationType.displayName ? newTitle : '';
handleFormChange('name', title);
await updateDestination(id, { ...formData, name: title });
const title = newTitle !== (item as ActualDestination).destinationType.displayName ? newTitle : '';

await updateDestination(id as string, { ...formData, name: title });
}
};

return (
<OverviewDrawer
title={item.name || item.destinationType.displayName}
imageUri={item.destinationType.imageUrl}
title={(item as ActualDestination).name || (item as ActualDestination).destinationType.displayName}
imageUri={(item as ActualDestination).destinationType.imageUrl}
isEdit={isEditing}
isFormDirty={isFormDirty}
onEdit={handleEdit}
Expand All @@ -135,10 +134,7 @@ export const DestinationDrawer: React.FC<Props> = () => {
/>
</FormContainer>
) : (
<DataContainer>
<ConditionDetails conditions={item.conditions} />
<CardDetails data={cardData} />
</DataContainer>
<CardDetails title='Destination Details' data={cardData} />
)}
</OverviewDrawer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { Dispatch, SetStateAction, useState } from 'react';
import styled from 'styled-components';
import { SignalUppercase } from '@/utils';
import type { DropdownOption } from '@/types';
import { Dropdown, Input, MonitoringCheckboxes } from '@/reuseable-components';

interface Props {
selectedTag: DropdownOption | undefined;
onTagSelect: (option: DropdownOption) => void;
onSearch: (value: string) => void;
selectedMonitors: SignalUppercase[];
setSelectedMonitors: Dispatch<SetStateAction<SignalUppercase[]>>;
}

const Container = styled.div`
display: flex;
align-items: center;
gap: 12px;
`;

const WidthConstraint = styled.div`
width: 160px;
margin-right: 8px;
`;

const DROPDOWN_OPTIONS = [
{ value: 'All types', id: 'all' },
{ value: 'Managed', id: 'managed' },
{ value: 'Self-hosted', id: 'self hosted' },
];

export const ChooseDestinationFilters: React.FC<Props> = ({ selectedTag, onTagSelect, onSearch, selectedMonitors, setSelectedMonitors }) => {
const [searchTerm, setSearchTerm] = useState('');

const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchTerm(value);
onSearch(value);
};

return (
<Container>
<WidthConstraint>
<Input placeholder='Search...' icon='/icons/common/search.svg' value={searchTerm} onChange={handleSearchChange} />
</WidthConstraint>
<WidthConstraint>
<Dropdown options={DROPDOWN_OPTIONS} value={selectedTag} onSelect={onTagSelect} onDeselect={() => {}} />
</WidthConstraint>
<MonitoringCheckboxes title='' selectedSignals={selectedMonitors} setSelectedSignals={setSelectedMonitors} />
</Container>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,58 @@ import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { SignalUppercase } from '@/utils';
import { useDestinationTypes } from '@/hooks';
import type { DestinationTypeItem } from '@/types';
import { DestinationsList } from './destinations-list';
import { Divider, Dropdown, Input, MonitoringCheckboxes, SectionTitle } from '@/reuseable-components';
import { Divider, SectionTitle } from '@/reuseable-components';
import type { DropdownOption, DestinationTypeItem } from '@/types';
import { ChooseDestinationFilters } from './choose-destination-filters';

interface Props {
onSelect: (item: DestinationTypeItem) => void;
hidden?: boolean;
}

const DEFAULT_MONITORS: SignalUppercase[] = ['LOGS', 'METRICS', 'TRACES'];
const DEFAULT_DROPDOWN_VALUE = { id: 'all', value: 'All types' };

const Container = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
`;

const Filters = styled.div`
display: flex;
align-items: center;
gap: 12px;
`;

const WidthConstraint = styled.div`
width: 160px;
margin-right: 8px;
`;

const DROPDOWN_OPTIONS = [
{ value: 'All types', id: 'all' },
{ value: 'Managed', id: 'managed' },
{ value: 'Self-hosted', id: 'self hosted' },
];

const DEFAULT_CATEGORY = DROPDOWN_OPTIONS[0];
const DEFAULT_MONITORS: SignalUppercase[] = ['LOGS', 'METRICS', 'TRACES'];

export const ChooseDestinationBody: React.FC<Props> = ({ onSelect, hidden }) => {
const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState(DEFAULT_CATEGORY);
export const ChooseDestinationBody: React.FC<Props> = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('');
const [selectedMonitors, setSelectedMonitors] = useState<SignalUppercase[]>(DEFAULT_MONITORS);
const [dropdownValue, setDropdownValue] = useState<DropdownOption>(DEFAULT_DROPDOWN_VALUE);

const { destinations: destinationTypes } = useDestinationTypes();
const { destinations } = useDestinationTypes();

const filteredDestinations = useMemo(() => {
return destinationTypes
return destinations
.map((category) => {
const filteredItems = category.items.filter((item) => {
const matchesSearch = !search || item.displayName.toLowerCase().includes(search.toLowerCase());
const matchesCategory = selectedCategory.id === 'all' || selectedCategory.id === category.name;
const matchesMonitor = selectedMonitors.some((monitor) => item.supportedSignals[monitor.toLowerCase()]?.supported);
const matchesSearch = searchValue ? item.displayName.toLowerCase().includes(searchValue.toLowerCase()) : true;
const matchesDropdown = dropdownValue.id !== 'all' ? category.name === dropdownValue.id : true;
const matchesMonitor = selectedMonitors.length ? selectedMonitors.some((monitor) => item.supportedSignals[monitor.toLowerCase()]?.supported) : true;

return matchesSearch && matchesCategory && matchesMonitor;
return matchesSearch && matchesDropdown && matchesMonitor;
});

return { ...category, items: filteredItems };
})
.filter(({ items }) => !!items.length); // Filter out empty categories
}, [destinationTypes, search, selectedCategory, selectedMonitors]);

if (hidden) return null;
.filter((category) => category.items.length > 0); // Filter out empty categories
}, [destinations, searchValue, dropdownValue, selectedMonitors]);

return (
<Container>
<SectionTitle title='Choose destination' description='Add backend destination you want to connect with Odigos.' />

<Filters>
<WidthConstraint>
<Input placeholder='Search...' icon='/icons/common/search.svg' value={search} onChange={({ target: { value } }) => setSearch(value)} />
</WidthConstraint>
<WidthConstraint>
<Dropdown options={DROPDOWN_OPTIONS} value={selectedCategory} onSelect={(opt) => setSelectedCategory(opt)} onDeselect={() => {}} />
</WidthConstraint>
<MonitoringCheckboxes title='' selectedSignals={selectedMonitors} setSelectedSignals={setSelectedMonitors} />
</Filters>

<ChooseDestinationFilters
selectedTag={dropdownValue}
onTagSelect={(opt) => setDropdownValue(opt)}
onSearch={setSearchValue}
selectedMonitors={selectedMonitors}
setSelectedMonitors={setSelectedMonitors}
/>
<Divider />

<DestinationsList items={filteredDestinations} setSelectedItems={onSelect} />
</Container>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,7 @@ export const DestinationModal: React.FC<AddDestinationModalProps> = ({ isOnboard
</SideMenuWrapper>

<ModalBody style={{ margin: '32px 24px 0 24px' }}>
{/*
in other modals we would render this out, but for this case we will use "hidden" instead,
this is to preserve the filters-state when going back-and-forth between selections
*/}
<ChooseDestinationBody onSelect={handleSelect} hidden={!!selectedItem} />

{!!selectedItem && (
{!!selectedItem ? (
<DestinationFormBody
destination={selectedItem}
isFormOk={isFormOk}
Expand All @@ -138,6 +132,8 @@ export const DestinationModal: React.FC<AddDestinationModalProps> = ({ isOnboard
dynamicFields={dynamicFields}
setDynamicFields={setDynamicFields}
/>
) : (
<ChooseDestinationBody onSelect={handleSelect} />
)}
</ModalBody>
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const RuleDrawer: React.FC<Props> = () => {
/>
</FormContainer>
) : (
<CardDetails data={cardData} />
<CardDetails title='Instrumentation Rule Details' data={cardData} />
)}
</OverviewDrawer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const SourceDrawer: React.FC<Props> = () => {
) : (
<DataContainer>
<ConditionDetails conditions={item.instrumentedApplicationDetails.conditions} />
<CardDetails data={cardData} />
<CardDetails title='Source Details' data={cardData} />
</DataContainer>
)}
</OverviewDrawer>
Expand Down

0 comments on commit 480dcb2

Please sign in to comment.