Skip to content

Commit

Permalink
feat: add destination drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
BenElferink committed Feb 9, 2025
1 parent 16e1631 commit 1ad84c6
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/containers/destination-drawer/build-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { DATA_CARD_FIELD_TYPES, type DataCardFieldsProps } from '@odigos/ui-components'
import { compareCondition, type Destination, type DestinationYamlProperties, DISPLAY_TITLES, safeJsonParse, SIGNAL_TYPE } from '@odigos/ui-utils'

const buildMonitorsList = (exportedSignals: Destination['exportedSignals']): string =>
Object.keys(exportedSignals)
.filter((key) => exportedSignals[key as SIGNAL_TYPE])
.join(', ')

const buildCard = (destination: Destination, yamlFields: DestinationYamlProperties[]) => {
const { exportedSignals, destinationType, fields } = destination

const arr: DataCardFieldsProps['data'] = [
{ title: DISPLAY_TITLES.NAME, value: destinationType.displayName },
{ title: DISPLAY_TITLES.TYPE, value: destinationType.type },
{ type: DATA_CARD_FIELD_TYPES.MONITORS, title: DISPLAY_TITLES.MONITORS, value: buildMonitorsList(exportedSignals) },
{ type: DATA_CARD_FIELD_TYPES.DIVIDER, width: '100%' },
]

const parsedFields = safeJsonParse<Record<string, string>>(fields, {})
const sortedParsedFields =
yamlFields.map((field) => ({ key: field.name, value: parsedFields[field.name] ?? null })).filter((item) => item.value !== null) ||
Object.entries(parsedFields).map(([key, value]) => ({ key, value }))

sortedParsedFields.map(({ key, value }) => {
const { displayName, secret, componentProperties, hideFromReadData, customReadDataLabels } = yamlFields.find((field) => field.name === key) || {}

const shouldHide = !!hideFromReadData?.length
? compareCondition(
hideFromReadData,
yamlFields.map((field) => ({ name: field.name, value: parsedFields[field.name] ?? null }))
)
: false

if (!shouldHide) {
const { type } = safeJsonParse(componentProperties, { type: '' })
const isSecret = (secret || type === 'password') && !!value.length ? new Array(10).fill('•').join('') : ''

if (!!customReadDataLabels?.length) {
customReadDataLabels.forEach(({ condition, ...custom }) => {
if (condition == value) {
arr.push({
title: custom.title,
value: custom.value,
})
}
})
} else {
arr.push({
title: displayName || key,
value: isSecret || value,
})
}
}
})

return arr
}

export { buildCard }
43 changes: 43 additions & 0 deletions src/containers/destination-drawer/destination-drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react'
import { useDrawerStore } from '../../store'
import type { StoryFn } from '@storybook/react'
import { DestinationDrawer, type DestinationDrawerProps } from '.'
import { ENTITY_TYPES, MOCK_DESTINATION_CATEGORIES, MOCK_DESTINATIONS, sleep } from '@odigos/ui-utils'

export default {
title: 'Containers/DestinationDrawer',
component: DestinationDrawer,
}

export const Default: StoryFn<DestinationDrawerProps> = (props) => {
const { setDrawerType, setDrawerEntityId } = useDrawerStore()

useEffect(() => {
setDrawerType(ENTITY_TYPES.DESTINATION)
setDrawerEntityId(MOCK_DESTINATIONS[0].id)
}, [])

const [testLoading, setTestLoading] = useState(props.testLoading || false)
const [testResult, setTestResult] = useState(props.testResult || undefined)

return (
<DestinationDrawer
{...props}
testLoading={testLoading}
testResult={testResult}
testConnection={async () => {
setTestLoading(true)
await sleep(1000)
setTestResult({ succeeded: true })
setTestLoading(false)
}}
/>
)
}

Default.args = {
categories: MOCK_DESTINATION_CATEGORIES,
destinations: MOCK_DESTINATIONS,
updateDestination: () => {},
deleteDestination: () => {},
}
189 changes: 189 additions & 0 deletions src/containers/destination-drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import React, { type FC, useMemo, useState } from 'react'
import styled from 'styled-components'
import { buildCard } from './build-card'
import { useDrawerStore } from '../../store'
import type { DestinationFormData } from '../../@types'
import { ConditionDetails, DataCard } from '@odigos/ui-components'
import { OverviewDrawer, useDestinationFormData } from '../../helpers'
import { DestinationForm, type DestinationFormProps } from '../destination-form'
import {
CRUD,
type Destination,
type DestinationCategories,
DestinationYamlProperties,
DISPLAY_TITLES,
ENTITY_TYPES,
safeJsonParse,
} from '@odigos/ui-utils'

interface DestinationDrawerProps {
categories: DestinationCategories
destinations: Destination[]
updateDestination: (id: string, destination: DestinationFormData) => void
deleteDestination: (id: string) => void
testConnection: DestinationFormProps['testConnection']
testLoading: DestinationFormProps['testLoading']
testResult: DestinationFormProps['testResult']
}

const FormContainer = styled.div`
width: 100%;
height: 100%;
max-height: calc(100vh - 220px);
overflow: overlay;
overflow-y: auto;
`

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

const DestinationDrawer: FC<DestinationDrawerProps> = ({
categories,
destinations,
updateDestination,
deleteDestination,
testConnection,
testLoading,
testResult,
}) => {
const { drawerType, drawerEntityId, setDrawerEntityId, setDrawerType } = useDrawerStore()

const isOpen = drawerType !== ENTITY_TYPES.DESTINATION
const onClose = () => {
setDrawerType(null)
setDrawerEntityId(null)
}

const [isEditing, setIsEditing] = useState(false)
const [isFormDirty, setIsFormDirty] = useState(false)
// const [thisItem, setThisItem] = useState<Destination | undefined>(undefined)

const {
formData,
formErrors,
handleFormChange,
resetFormData,
validateForm,
loadFormWithDrawerItem,
yamlFields,
setYamlFields,
dynamicFields,
setDynamicFields,
} = useDestinationFormData({
// preLoadedFields: thisItem?.fields,
// TODO: supportedSignals: thisDestination?.supportedSignals,
// currently, the real "supportedSignals" is being used by "destination" passed as prop to "DestinationFormBody"
})

const thisItem = useMemo(() => {
if (isOpen) return null

const found = destinations?.find((x) => x.id === drawerEntityId)
if (!!found) {
loadFormWithDrawerItem(found)

const fields: DestinationYamlProperties[] = []
const parsedCategories: typeof categories = JSON.parse(JSON.stringify(categories))

for (const category of parsedCategories) {
const autoFilledFields = safeJsonParse<{ [key: string]: string }>(found.fields, {})
const idx = category.items.findIndex((item) => item.type === found.destinationType.type)

if (idx !== -1) {
fields.push(
...category.items[idx].fields.map((field) => ({
...field,
initialValue: autoFilledFields[field.name],
}))
)
}
}

setYamlFields(fields)
}

return found
}, [isOpen, drawerEntityId, destinations])

if (!thisItem) return null

const thisOptionType = categories
.map(({ items }) => items.filter(({ type }) => type === thisItem.destinationType.type))
.filter((arr) => !!arr.length)?.[0]?.[0]

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

const handleCancel = () => {
setIsEditing(false)
setIsFormDirty(false)
loadFormWithDrawerItem(thisItem)
}

const handleDelete = () => {
deleteDestination(drawerEntityId as string)
setIsEditing(false)
setIsFormDirty(false)
resetFormData()
// close drawer, all other cases are handled in OverviewDrawer
onClose()
}

const handleSave = (newTitle: string) => {
if (validateForm({ withAlert: true, alertTitle: CRUD.UPDATE })) {
const title = newTitle !== thisItem.destinationType.displayName ? newTitle : ''
handleFormChange('name', title)
updateDestination(drawerEntityId as string, { ...formData, name: title })
setIsEditing(false)
setIsFormDirty(false)
}
}

return (
<OverviewDrawer
title={thisItem.name || thisItem.destinationType.displayName}
iconSrc={thisItem.destinationType.imageUrl}
isEdit={isEditing}
isFormDirty={isFormDirty}
onEdit={handleEdit}
onSave={handleSave}
onDelete={handleDelete}
onCancel={handleCancel}
>
{isEditing ? (
<FormContainer>
<DestinationForm
isUpdate
categoryItem={thisOptionType}
formData={formData}
formErrors={formErrors}
handleFormChange={(...params) => {
setIsFormDirty(true)
handleFormChange(...params)
}}
dynamicFields={dynamicFields}
setDynamicFields={(...params) => {
setIsFormDirty(true)
setDynamicFields(...params)
}}
validateForm={validateForm}
testConnection={testConnection}
testLoading={testLoading}
testResult={testResult}
/>
</FormContainer>
) : (
<DataContainer>
<ConditionDetails conditions={thisItem.conditions || []} />
<DataCard title={DISPLAY_TITLES.DESTINATION_DETAILS} data={!!thisItem ? buildCard(thisItem, yamlFields) : []} />
</DataContainer>
)}
</OverviewDrawer>
)
}

export { DestinationDrawer, type DestinationDrawerProps }
2 changes: 2 additions & 0 deletions src/containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export * from './action-modal/index'
export * from './cli-drawer/index'
export * from './data-flow/index'
export * from './data-flow-actions-menu/index'
export * from './destination-drawer/index'
export * from './destination-form/index'
export * from './destination-modal/index'
export * from './instrumentation-rule-drawer/index'
export * from './instrumentation-rule-form/index'
export * from './instrumentation-rule-modal/index'
Expand Down

0 comments on commit 1ad84c6

Please sign in to comment.