Skip to content

Commit 710bd3b

Browse files
committed
feat: ChartStore Details - revamp layout, add Readme, About section
1 parent 268904d commit 710bd3b

File tree

15 files changed

+750
-74
lines changed

15 files changed

+750
-74
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { useEffect, useMemo, useState } from 'react'
2+
import { Route, Switch, useRouteMatch } from 'react-router-dom'
3+
4+
import {
5+
APIResponseHandler,
6+
BreadCrumb,
7+
PageHeader,
8+
SegmentedControl,
9+
SegmentedControlProps,
10+
SelectPickerProps,
11+
useAsync,
12+
useBreadcrumb,
13+
useUrlFilters,
14+
} from '@devtron-labs/devtron-fe-common-lib'
15+
16+
import { ChartSelector } from '@Components/AppSelector'
17+
import { URLS } from '@Config/routes'
18+
import { getAvailableCharts } from '@Services/service'
19+
20+
import { ChartDetailsAbout } from './ChartDetailsAbout'
21+
import { ChartDetailsDeploy } from './ChartDetailsDeploy'
22+
import { ChartDetailsReadme } from './ChartDetailsReadme'
23+
import { CHART_DETAILS_SEGMENTS } from './constants'
24+
import { fetchChartDetails, fetchChartVersions } from './services'
25+
import { ChartDetailsSearchParams, ChartDetailsSegment } from './types'
26+
import { chartSelectorFilterOption, chartSelectorFormatOptionLabel, parseChartDetailsSearchParams } from './utils'
27+
28+
import './chartDetails.scss'
29+
30+
export const ChartDetails = () => {
31+
// STATES
32+
const [selectedChartVersion, setSelectedChartVersion] = useState<number>(null)
33+
34+
// HOOKS
35+
const {
36+
path,
37+
params: { chartId },
38+
} = useRouteMatch<{ chartId: string; chartSegment: ChartDetailsSegment }>()
39+
40+
const { tab, updateSearchParams } = useUrlFilters<void, ChartDetailsSearchParams>({
41+
parseSearchParams: parseChartDetailsSearchParams,
42+
})
43+
44+
// ASYNC CALLS
45+
const [isFetchingChartVersions, chartVersions, chartVersionsErr, reloadChartVersions] = useAsync(
46+
() => fetchChartVersions(chartId),
47+
[chartId],
48+
)
49+
50+
const [isFetchingChartDetails, chartDetails, chartDetailsErr, reloadChartDetails] = useAsync(
51+
() => fetchChartDetails(selectedChartVersion),
52+
[selectedChartVersion],
53+
!!selectedChartVersion,
54+
{ resetOnChange: false },
55+
)
56+
57+
useEffect(() => {
58+
if (!isFetchingChartVersions && chartVersions) {
59+
setSelectedChartVersion(chartVersions[0].id)
60+
}
61+
}, [isFetchingChartVersions, chartVersions])
62+
63+
// CONFIGS
64+
const { breadcrumbs } = useBreadcrumb(
65+
{
66+
alias: {
67+
':chartSegment?': null,
68+
':chartId': {
69+
component: (
70+
<ChartSelector
71+
primaryKey="chartId"
72+
primaryValue="name"
73+
api={getAvailableCharts}
74+
matchedKeys={[]}
75+
apiPrimaryKey="id"
76+
formatOptionLabel={chartSelectorFormatOptionLabel}
77+
filterOption={chartSelectorFilterOption}
78+
/>
79+
),
80+
linked: false,
81+
},
82+
chart: null,
83+
'chart-store': null,
84+
},
85+
},
86+
[chartId],
87+
)
88+
89+
const chartOptions = useMemo(
90+
() =>
91+
(chartVersions ?? []).map(({ id, version }) => ({
92+
label: version.startsWith('v') ? version : `v${version}`,
93+
value: id,
94+
})),
95+
[JSON.stringify(chartVersions)],
96+
)
97+
98+
// HANDLERS
99+
const handleSegmentChange: SegmentedControlProps['onChange'] = (selectedSegment) => {
100+
updateSearchParams({ tab: selectedSegment.value as ChartDetailsSegment })
101+
}
102+
103+
const handleChartChange: SelectPickerProps<number>['onChange'] = ({ value }) => {
104+
setSelectedChartVersion(value)
105+
}
106+
107+
// RENDERERS
108+
const renderBreadcrumbs = () => <BreadCrumb breadcrumbs={breadcrumbs} />
109+
110+
const renderSegments = () => {
111+
switch (tab) {
112+
case ChartDetailsSegment.README:
113+
return (
114+
<ChartDetailsReadme
115+
chartName={chartDetails?.name}
116+
readme={chartDetails?.readme}
117+
chartsOptions={chartOptions}
118+
selectedChartVersion={selectedChartVersion}
119+
onChartChange={handleChartChange}
120+
isLoading={isFetchingChartDetails}
121+
error={chartDetailsErr}
122+
reload={reloadChartDetails}
123+
/>
124+
)
125+
case ChartDetailsSegment.PRESET_VALUES:
126+
return <div>PRESET VALUES</div>
127+
case ChartDetailsSegment.DEPLOYMENTS:
128+
return <div>DEPLOYMENTS</div>
129+
default:
130+
return null
131+
}
132+
}
133+
134+
return (
135+
<div className="flex-grow-1 flexbox-col bg__secondary dc__overflow-hidden">
136+
<PageHeader isBreadcrumbs breadCrumbs={renderBreadcrumbs} />
137+
138+
<APIResponseHandler
139+
isLoading={isFetchingChartVersions}
140+
progressingProps={{ pageLoader: true }}
141+
error={chartVersionsErr}
142+
errorScreenManagerProps={{ code: chartVersionsErr?.code, reload: reloadChartVersions }}
143+
>
144+
<Switch>
145+
<Route path={`${path}${URLS.DEPLOY_CHART}/:presetValueId?`}>
146+
<ChartDetailsDeploy
147+
chartDetails={chartDetails}
148+
chartVersions={chartVersions}
149+
selectedChartVersion={selectedChartVersion}
150+
/>
151+
</Route>
152+
<Route>
153+
<div className="chart-details flex-grow-1 p-20 dc__overflow-auto">
154+
<div className="flex column left top dc__gap-16 mw-none">
155+
<SegmentedControl
156+
name="chart-details-segmented-control"
157+
segments={CHART_DETAILS_SEGMENTS}
158+
value={tab}
159+
onChange={handleSegmentChange}
160+
/>
161+
{renderSegments()}
162+
</div>
163+
<ChartDetailsAbout isLoading={isFetchingChartDetails} chartDetails={chartDetails} />
164+
</div>
165+
</Route>
166+
</Switch>
167+
</APIResponseHandler>
168+
</div>
169+
)
170+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { useMemo } from 'react'
2+
import { useRouteMatch } from 'react-router-dom'
3+
import moment from 'moment'
4+
5+
import {
6+
Button,
7+
ButtonComponentType,
8+
ComponentSizeType,
9+
handleAnalyticsEvent,
10+
Icon,
11+
InfoBlock,
12+
} from '@devtron-labs/devtron-fe-common-lib'
13+
14+
import { ChartDetailsAboutProps } from './types'
15+
import { getParsedChartYAML } from './utils'
16+
17+
const AboutHeaderLoadingSkeleton = () => (
18+
<div className="flexbox-col dc__gap-16">
19+
<div className="shimmer-loading loading-icon" />
20+
<div className="shimmer-loading w-150 h-20" />
21+
<div className="flexbox-col dc__gap-8">
22+
<div className="shimmer-loading w-250 h-16" />
23+
<div className="shimmer-loading w-100 h-16" />
24+
</div>
25+
</div>
26+
)
27+
28+
const AboutBodyLoadingSkeleton = () => (
29+
<div className="flexbox-col dc__gap-12">
30+
<div className="shimmer-loading w-250 h-16" />
31+
<div className="shimmer-loading w-200 h-16" />
32+
<div className="shimmer-loading w-100 h-16" />
33+
</div>
34+
)
35+
36+
const ChartMetaData = ({
37+
title,
38+
subtitle,
39+
isLink,
40+
}: {
41+
title: string
42+
subtitle: string | string[]
43+
isLink?: boolean
44+
}) => {
45+
const renderSubtitle = () => {
46+
if (Array.isArray(subtitle)) {
47+
return subtitle.map((item) =>
48+
isLink && item ? (
49+
<a className="m-0 fs-13 lh-20 fw-4 dc__break-word" href={item} target="_blank" rel="noreferrer">
50+
{item}
51+
</a>
52+
) : (
53+
<p key={item} className="m-0 fs-13 lh-20 fw-4 cn-9 dc__break-word">
54+
{item}
55+
</p>
56+
),
57+
)
58+
}
59+
60+
return isLink ? (
61+
<a className="m-0 fs-13 lh-20 fw-4 dc__break-word" href={subtitle} target="_blank" rel="noreferrer">
62+
{subtitle}
63+
</a>
64+
) : (
65+
<p className="m-0 fs-13 lh-20 fw-4 cn-9 dc__break-word">{subtitle}</p>
66+
)
67+
}
68+
69+
return (
70+
<div className="flexbox-col dc__gap-4">
71+
<h5 className="m-0 fs-13 lh-20 fw-4 cn-7">{title}</h5>
72+
{renderSubtitle()}
73+
</div>
74+
)
75+
}
76+
77+
export const ChartDetailsAbout = ({ chartDetails, isLoading }: ChartDetailsAboutProps) => {
78+
const { url } = useRouteMatch()
79+
80+
const {
81+
icon,
82+
name,
83+
description,
84+
chartName,
85+
created,
86+
appVersion,
87+
deprecated,
88+
digest,
89+
home,
90+
isOCICompliantChart,
91+
chartYaml,
92+
} = chartDetails ?? {}
93+
94+
const parsedChartYaml = useMemo(() => getParsedChartYAML(chartYaml), [chartYaml])
95+
96+
const handleDeploy = () => {
97+
handleAnalyticsEvent({ category: 'Chart Store', action: 'CS_CHART_CONFIGURE_&_DEPLOY' })
98+
}
99+
100+
if (isLoading && !chartDetails) {
101+
return (
102+
<div className="flexbox-col dc__gap-20 mw-none">
103+
<AboutHeaderLoadingSkeleton />
104+
<div className="divider__secondary--horizontal" />
105+
<AboutBodyLoadingSkeleton />
106+
</div>
107+
)
108+
}
109+
110+
return (
111+
<div className="flexbox-col dc__gap-20 mw-none">
112+
<div className="flexbox-col dc__gap-12">
113+
{icon ? (
114+
<div className="icon-dim-48">
115+
<img src={icon} className="w-100" alt="chart-icon" data-testid="chart-type-image" />
116+
</div>
117+
) : (
118+
<Icon name="ic-helm" color="N700" size={48} />
119+
)}
120+
<h2 className="m-0 fs-16 lh-24 fw-6 cn-9">{name}</h2>
121+
<p className="m-0 fs-13 lh-20 cn-9">{description}</p>
122+
</div>
123+
<div>
124+
<Button
125+
dataTestId="deploy-chart-button"
126+
startIcon={<Icon name="ic-rocket-launch" color={null} />}
127+
text="Deploy Chart..."
128+
size={ComponentSizeType.medium}
129+
component={ButtonComponentType.link}
130+
linkProps={{
131+
to: `${url}/deploy-chart`,
132+
}}
133+
onClick={handleDeploy}
134+
/>
135+
</div>
136+
<div className="divider__secondary--horizontal" />
137+
{isLoading ? (
138+
<AboutBodyLoadingSkeleton />
139+
) : (
140+
<>
141+
{deprecated && (
142+
<InfoBlock
143+
variant="warning"
144+
heading="Deprecated Chart"
145+
description="This chart is no longer maintained and may not receive updates or support from the publisher."
146+
/>
147+
)}
148+
<div className="flexbox-col dc__gap-12">
149+
<ChartMetaData title="Chart repository" subtitle={chartName} />
150+
<ChartMetaData title="Home" subtitle={home} isLink />
151+
<ChartMetaData
152+
title="Application Version"
153+
subtitle={appVersion?.startsWith('v') ? appVersion.slice(1) : appVersion}
154+
/>
155+
<ChartMetaData
156+
title="Created"
157+
subtitle={isOCICompliantChart ? '-' : moment(created).fromNow()}
158+
/>
159+
<ChartMetaData title="Digest" subtitle={digest} />
160+
{!!parsedChartYaml && (
161+
<ChartMetaData title="Source" subtitle={parsedChartYaml.sources} isLink />
162+
)}
163+
</div>
164+
</>
165+
)}
166+
</div>
167+
)
168+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useMemo } from 'react'
2+
import { useParams } from 'react-router-dom'
3+
4+
import { APIResponseHandler, useAsync } from '@devtron-labs/devtron-fe-common-lib'
5+
6+
import ChartValuesView from '@Components/v2/values/chartValuesDiff/ChartValuesView'
7+
import { ChartInstalledConfig } from '@Components/v2/values/chartValuesDiff/ChartValuesView.type'
8+
9+
import { fetchChartValuesList } from './services'
10+
import { ChartDetailsDeployProps } from './types'
11+
12+
export const ChartDetailsDeploy = ({ chartDetails, chartVersions, selectedChartVersion }: ChartDetailsDeployProps) => {
13+
const { chartId } = useParams<{ chartId: string }>()
14+
15+
const [isFetchingChartValuesList, chartValuesList, chartValuesListErr, reloadChartValuesList] = useAsync(
16+
() => fetchChartValuesList(chartId),
17+
[chartId],
18+
)
19+
20+
const chartValues = useMemo(
21+
() =>
22+
(chartValuesList ?? []).find(
23+
(chartValue) => chartValue.kind === 'DEFAULT' && chartValue.id === selectedChartVersion,
24+
),
25+
[selectedChartVersion, chartValuesList],
26+
)
27+
28+
const isChartValuesViewLoading =
29+
!chartDetails?.chartName || isFetchingChartValuesList || !chartValuesList.length || !chartVersions.length
30+
31+
return (
32+
<APIResponseHandler
33+
isLoading={isChartValuesViewLoading}
34+
progressingProps={{ pageLoader: true }}
35+
error={chartValuesListErr}
36+
errorScreenManagerProps={{ code: chartValuesListErr?.code, reload: reloadChartValuesList }}
37+
>
38+
<ChartValuesView
39+
isDeployChartView
40+
installedConfigFromParent={chartDetails as unknown as ChartInstalledConfig}
41+
chartValuesListFromParent={chartValuesList}
42+
chartVersionsDataFromParent={chartVersions}
43+
chartValuesFromParent={chartValues}
44+
selectedVersionFromParent={selectedChartVersion}
45+
/>
46+
</APIResponseHandler>
47+
)
48+
}

0 commit comments

Comments
 (0)