Skip to content

Commit

Permalink
feat: add source drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
BenElferink committed Feb 9, 2025
1 parent c3710b8 commit 65f028b
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 10 deletions.
4 changes: 3 additions & 1 deletion src/@types/sources.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export interface SourceFormData {}
export interface SourceFormData {
otelServiceName: string
}
1 change: 1 addition & 0 deletions src/containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from './instrumentation-rule-form/index'
export * from './instrumentation-rule-modal/index'
export * from './multi-source-control/index'
export * from './notification-manager/index'
export * from './source-drawer/index'
export * from './source-form/index'
export * from './toast-list/index'
16 changes: 16 additions & 0 deletions src/containers/source-drawer/build-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DISPLAY_TITLES, type Source } from '@odigos/ui-utils'
import type { DataCardFieldsProps } from '@odigos/ui-components'

const buildCard = (source: Source) => {
const { name, kind, namespace } = source

const arr: DataCardFieldsProps['data'] = [
{ title: DISPLAY_TITLES.NAMESPACE, value: namespace },
{ title: DISPLAY_TITLES.KIND, value: kind },
{ title: DISPLAY_TITLES.NAME, value: name, tooltip: 'K8s resource name' },
]

return arr
}

export { buildCard }
215 changes: 215 additions & 0 deletions src/containers/source-drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import React, { type FC, useMemo, useState } from 'react'
import styled from 'styled-components'
import { buildCard } from './build-card'
import { SourceForm } from '../source-form'
import { useDrawerStore } from '../../store'
import type { SourceFormData } from '../../@types'
import { CodeIcon, ListIcon } from '@odigos/ui-icons'
import { OverviewDrawer, useSourceFormData } from '../../helpers'
import { ConditionDetails, DATA_CARD_FIELD_TYPES, DataCard, type DataCardFieldsProps, Segment } from '@odigos/ui-components'
import { type DescribeSource, DISPLAY_TITLES, ENTITY_TYPES, getEntityIcon, safeJsonStringify, type Source, type WorkloadId } from '@odigos/ui-utils'

interface SourceDrawerProps {
sources: Source[]
persistSources: (
selectAppsList: { [namespace: string]: Pick<Source, 'name' | 'kind' | 'selected'>[] },
futureSelectAppsList: { [namespace: string]: boolean }
) => Promise<void>
updateSource: (sourceId: WorkloadId, payload: SourceFormData) => Promise<void>
describe: DescribeSource
}

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 SourceDrawer: FC<SourceDrawerProps> = ({ sources, persistSources, updateSource, describe }) => {
const { drawerType, drawerEntityId, setDrawerEntityId, setDrawerType } = useDrawerStore()

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

const [isEditing, setIsEditing] = useState(false)
const [isFormDirty, setIsFormDirty] = useState(false)
const [isPrettyMode, setIsPrettyMode] = useState(true) // for "describe source"

const { formData, handleFormChange, resetFormData, loadFormWithDrawerItem } = useSourceFormData()
// const { data: describe, restructureForPrettyMode } = useDescribeSource(drawerEntityId as WorkloadId)

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

const found = sources?.find(
(x) =>
x.namespace === (drawerEntityId as WorkloadId).namespace &&
x.name === (drawerEntityId as WorkloadId).name &&
x.kind === (drawerEntityId as WorkloadId).kind
)
if (!!found) loadFormWithDrawerItem(found)

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

if (!thisItem) return null

const containersData =
thisItem.containers?.map(
(container) =>
({
type: DATA_CARD_FIELD_TYPES.SOURCE_CONTAINER,
width: '100%',
value: JSON.stringify(container),
} as DataCardFieldsProps['data'][0])
) || []

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

const handleCancel = () => {
setIsEditing(false)
setIsFormDirty(false)
handleFormChange('otelServiceName', thisItem.otelServiceName || thisItem.name || '')
}

const handleDelete = async () => {
const { namespace } = thisItem
await persistSources({ [namespace]: [{ ...thisItem, selected: false }] }, {})
setIsEditing(false)
setIsFormDirty(false)
resetFormData()
// close drawer, all other cases are handled in OverviewDrawer
onClose()
}

const handleSave = async () => {
const title = formData.otelServiceName !== thisItem.name ? formData.otelServiceName : ''
handleFormChange('otelServiceName', title)
await updateSource(drawerEntityId as WorkloadId, { ...formData, otelServiceName: title })
setIsEditing(false)
setIsFormDirty(false)
}

// This function is used to restructure the data, so that it reflects the output given by "odigos describe" command in the CLI.
// This is not really needed, but it's a nice-to-have feature to make the data more readable.
const restructureForPrettyMode = () => {
if (!describe) return {}

const payload: Record<string, any> = {}

const mapObjects = (obj: any, category?: string, options?: { keyPrefix?: string }) => {
if (typeof obj === 'object' && !!obj?.name) {
let key = options?.keyPrefix ? `${options?.keyPrefix}${obj.name}` : obj.name
let val = obj.value

if (obj.explain) key += `@tooltip=${obj.explain}`
if (obj.status) val += `@status=${obj.status}`
else val += '@status=none'

if (!!category && !payload[category]) payload[category] = {}
if (!!category) payload[category][key] = val
else payload[key] = val
}
}

Object.values(describe).forEach((val) => mapObjects(val))
Object.values(describe?.sourceObjects || {}).forEach((val) => mapObjects(val, 'Sources'))
Object.values(describe?.otelAgents || {}).forEach((val) => mapObjects(val, 'Instrumentation Config'))
describe.otelAgents?.containers?.forEach((obj, i) =>
Object.values(obj).forEach((val) => mapObjects(val, 'Instrumentation Config', { keyPrefix: `Container #${i + 1} - ` }))
)
describe.runtimeInfo?.containers?.forEach((obj, i) =>
Object.values(obj).forEach((val) => mapObjects(val, 'Runtime Info', { keyPrefix: `Container #${i + 1} - ` }))
)

payload['Pods'] = { 'Total Pods': `${describe.totalPods}@status=none` }
describe.pods.forEach((obj) => {
Object.values(obj).forEach((val) => mapObjects(val, 'Pods'))
obj.containers?.forEach((containers, i) => {
Object.values(containers).forEach((val) => mapObjects(val, 'Pods', { keyPrefix: `Container #${i + 1} - ` }))
containers?.instrumentationInstances.forEach((obj, i) =>
Object.values(obj).forEach((val) => mapObjects(val, 'Pods', { keyPrefix: `Instrumentation Instance #${i + 1} - ` }))
)
})
})

return payload
}

return (
<OverviewDrawer
title={thisItem.otelServiceName || thisItem.name}
titleTooltip='This attribute is used to identify the name of the service (service.name) that is generating telemetry data.'
icon={getEntityIcon(ENTITY_TYPES.SOURCE)}
isEdit={isEditing}
isFormDirty={isFormDirty}
onEdit={handleEdit}
onSave={handleSave}
onDelete={handleDelete}
onCancel={handleCancel}
isLastItem={sources.length === 1}
>
{isEditing ? (
<FormContainer>
<SourceForm
formData={formData}
handleFormChange={(...params) => {
setIsFormDirty(true)
handleFormChange(...params)
}}
/>
</FormContainer>
) : (
<DataContainer>
<ConditionDetails conditions={thisItem.conditions || []} />
<DataCard title={DISPLAY_TITLES.SOURCE_DETAILS} data={!!thisItem ? buildCard(thisItem) : []} />
<DataCard
title={DISPLAY_TITLES.DETECTED_CONTAINERS}
titleBadge={containersData.length}
description={DISPLAY_TITLES.DETECTED_CONTAINERS_DESCRIPTION}
data={containersData}
/>
<DataCard
title={DISPLAY_TITLES.DESCRIBE_SOURCE}
action={
<Segment
options={[
{ icon: ListIcon, value: true },
{ icon: CodeIcon, value: false },
]}
selected={isPrettyMode}
setSelected={setIsPrettyMode}
/>
}
data={[
{
type: DATA_CARD_FIELD_TYPES.CODE,
value: JSON.stringify({
language: 'json',
code: safeJsonStringify(isPrettyMode ? restructureForPrettyMode() : describe),
pretty: isPrettyMode,
}),
width: 'inherit',
},
]}
/>
</DataContainer>
)}
</OverviewDrawer>
)
}

export { SourceDrawer, type SourceDrawerProps }
28 changes: 28 additions & 0 deletions src/containers/source-drawer/source-drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useEffect } from 'react'
import { useDrawerStore } from '../../store'
import type { StoryFn } from '@storybook/react'
import { SourceDrawer, type SourceDrawerProps } from '.'
import { ENTITY_TYPES, MOCK_DESCRIBE_SOURCE, MOCK_SOURCES } from '@odigos/ui-utils'

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

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

useEffect(() => {
setDrawerType(ENTITY_TYPES.SOURCE)
setDrawerEntityId({ namespace: MOCK_SOURCES[0].namespace, name: MOCK_SOURCES[0].name, kind: MOCK_SOURCES[0].kind })
}, [])

return <SourceDrawer {...props} />
}

Default.args = {
sources: MOCK_SOURCES,
persistSources: async () => {},
updateSource: async () => {},
describe: MOCK_DESCRIBE_SOURCE,
}
9 changes: 3 additions & 6 deletions src/containers/source-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React, { type FC } from 'react'
import styled from 'styled-components'
import { Input } from '@odigos/ui-components'

interface Form {
otelServiceName: string
}
import type { SourceFormData } from '../../@types'

interface SourceFormProps {
formData: Form
handleFormChange: (key: keyof Form, val: any) => void
formData: SourceFormData
handleFormChange: (key: keyof SourceFormData, val: any) => void
}

const Container = styled.div`
Expand Down
7 changes: 4 additions & 3 deletions src/containers/source-form/source-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { useState } from 'react'
import React from 'react'
import type { StoryFn } from '@storybook/react'
import { SourceForm, type SourceFormProps } from '.'
import { useSourceFormData } from '../../helpers'

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

export const Default: StoryFn<SourceFormProps> = (props) => {
const [formData, setFormData] = useState({ otelServiceName: '' })
const { formData, handleFormChange } = useSourceFormData()

return <SourceForm {...props} formData={formData} handleFormChange={(k, v) => setFormData((prev) => ({ ...prev, [k]: v }))} />
return <SourceForm {...props} formData={formData} handleFormChange={handleFormChange} />
}

Default.args = {}
1 change: 1 addition & 0 deletions src/helpers/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './use-click-node'
export * from './use-click-notification'
export * from './use-destination-form-data'
export * from './use-instrumentation-rule-form-data'
export * from './use-source-form-data'
50 changes: 50 additions & 0 deletions src/helpers/hooks/use-source-form-data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useNotificationStore } from '../../../store'
import type { SourceFormData } from '../../../@types'
import { FORM_ALERTS, NOTIFICATION_TYPE, type Source, useGenericForm } from '@odigos/ui-utils'

const INITIAL: SourceFormData = {
otelServiceName: '',
}

export const useSourceFormData = () => {
const { addNotification } = useNotificationStore()
const { formData, formErrors, handleFormChange, handleErrorChange, resetFormData } = useGenericForm<SourceFormData>(INITIAL)

const validateForm = (params?: { withAlert?: boolean; alertTitle?: string }) => {
const errors: typeof formErrors = {}
let ok = true

// Sources don't have any specific validations yet, no required fields at this time

if (!ok && params?.withAlert) {
addNotification({
type: NOTIFICATION_TYPE.WARNING,
title: params.alertTitle,
message: FORM_ALERTS.REQUIRED_FIELDS,
hideFromHistory: true,
})
}

handleErrorChange(undefined, undefined, errors)

return ok
}

const loadFormWithDrawerItem = ({ otelServiceName, name }: Source) => {
const updatedData: SourceFormData = {
...INITIAL,
otelServiceName: otelServiceName || name || '',
}

handleFormChange(undefined, undefined, updatedData)
}

return {
formData,
formErrors,
handleFormChange,
resetFormData,
validateForm,
loadFormWithDrawerItem,
}
}

0 comments on commit 65f028b

Please sign in to comment.