|
| 1 | +import { useEffect, useMemo, useState } from 'react' |
| 2 | +import { NavLink } from 'react-router-dom' |
| 3 | + |
| 4 | +import { |
| 5 | + Button, |
| 6 | + ButtonStyleType, |
| 7 | + ButtonVariantType, |
| 8 | + ComponentSizeType, |
| 9 | + EMPTY_STATE_STATUS, |
| 10 | + GenericEmptyState, |
| 11 | + handleAnalyticsEvent, |
| 12 | + Icon, |
| 13 | + ImageType, |
| 14 | + InfoBlock, |
| 15 | + Popover, |
| 16 | + Progressing, |
| 17 | + SearchBar, |
| 18 | + ToastManager, |
| 19 | + ToastVariantType, |
| 20 | + usePopover, |
| 21 | + useQuery, |
| 22 | +} from '@devtron-labs/devtron-fe-common-lib' |
| 23 | + |
| 24 | +import { reSyncChartRepo } from '@Components/chartRepo/chartRepo.service' |
| 25 | +import { TOAST_INFO } from '@Config/constantMessaging' |
| 26 | +import { URLS } from '@Config/routes' |
| 27 | + |
| 28 | +import AddChartSource from './AddChartSource' |
| 29 | +import ChartListPopUpRow from './ChartListPopUpRow' |
| 30 | +import { ChartsListProps } from './types' |
| 31 | + |
| 32 | +export const ChartsList = ({ chartsList, isLoading }: ChartsListProps) => { |
| 33 | + // STATES |
| 34 | + const [searchText, setSearchText] = useState('') |
| 35 | + const [chartActiveMap, setChartActiveMap] = useState<Record<string, boolean>>({}) |
| 36 | + |
| 37 | + // HOOKS |
| 38 | + const { open, overlayProps, popoverProps, scrollableRef, triggerProps, closePopover } = usePopover({ |
| 39 | + id: 'chart-list-popover', |
| 40 | + alignment: 'middle', |
| 41 | + width: 400, |
| 42 | + }) |
| 43 | + |
| 44 | + // QUERIES |
| 45 | + const { isFetching, isSuccess, refetch } = useQuery({ |
| 46 | + queryKey: ['reSyncChartRepo'], |
| 47 | + queryFn: reSyncChartRepo, |
| 48 | + enabled: false, |
| 49 | + }) |
| 50 | + |
| 51 | + useEffect(() => { |
| 52 | + setChartActiveMap( |
| 53 | + (chartsList ?? []).reduce((acc, curr) => { |
| 54 | + acc[curr.name] = curr.active |
| 55 | + return acc |
| 56 | + }, {}), |
| 57 | + ) |
| 58 | + }, [chartsList]) |
| 59 | + |
| 60 | + useEffect(() => { |
| 61 | + if (!open) { |
| 62 | + setSearchText('') |
| 63 | + } |
| 64 | + }, [open]) |
| 65 | + |
| 66 | + useEffect(() => { |
| 67 | + if (!isFetching && isSuccess) { |
| 68 | + ToastManager.showToast({ |
| 69 | + variant: ToastVariantType.success, |
| 70 | + description: TOAST_INFO.RE_SYNC, |
| 71 | + }) |
| 72 | + } |
| 73 | + }, [isFetching, isSuccess]) |
| 74 | + |
| 75 | + // COMPUTED VALUES |
| 76 | + const filteredChartList = useMemo( |
| 77 | + () => (chartsList ?? []).filter((chart) => chart.name.toLowerCase().indexOf(searchText.toLowerCase()) >= 0), |
| 78 | + [chartsList, searchText], |
| 79 | + ) |
| 80 | + |
| 81 | + // HANDLERS |
| 82 | + const handleSourceBtnClick = () => { |
| 83 | + handleAnalyticsEvent({ category: 'Chart Store', action: 'CS_SOURCE' }) |
| 84 | + } |
| 85 | + |
| 86 | + const toggleEnabled = (key: string) => (enabled: boolean) => |
| 87 | + setChartActiveMap({ ...chartActiveMap, [key]: enabled }) |
| 88 | + |
| 89 | + const handleSearchEnter = (searchKey: string) => { |
| 90 | + setSearchText(searchKey) |
| 91 | + } |
| 92 | + |
| 93 | + const handleRefetchCharts = async () => { |
| 94 | + await refetch() |
| 95 | + } |
| 96 | + |
| 97 | + // RENDERERS |
| 98 | + const renderChartListBody = () => { |
| 99 | + if (isLoading) { |
| 100 | + return ( |
| 101 | + <div ref={scrollableRef} className="flex column dc__gap-12 flex-grow-1"> |
| 102 | + <Progressing size={24} /> |
| 103 | + <span className="dc__loading-dots">Loading Chart source</span> |
| 104 | + </div> |
| 105 | + ) |
| 106 | + } |
| 107 | + |
| 108 | + if (!chartsList.length) { |
| 109 | + return ( |
| 110 | + <div ref={scrollableRef} className="flex-grow-1 flex"> |
| 111 | + <GenericEmptyState |
| 112 | + imgName="img-no-result" |
| 113 | + title={EMPTY_STATE_STATUS.CHART.NO_SOURCE_TITLE} |
| 114 | + subTitle={ |
| 115 | + <div> |
| 116 | + <span>Add a </span> |
| 117 | + <NavLink to={URLS.GLOBAL_CONFIG_CHART}>Chart repositories</NavLink> |
| 118 | + <span> or </span> |
| 119 | + <NavLink to={URLS.GLOBAL_CONFIG_DOCKER}>OCI Registries</NavLink> |
| 120 | + <span>to view and deploy helm charts.</span> |
| 121 | + </div> |
| 122 | + } |
| 123 | + imageType={ImageType.Medium} |
| 124 | + /> |
| 125 | + </div> |
| 126 | + ) |
| 127 | + } |
| 128 | + |
| 129 | + return ( |
| 130 | + <> |
| 131 | + <SearchBar |
| 132 | + dataTestId="chart-store-search-box" |
| 133 | + initialSearchText={searchText} |
| 134 | + containerClassName="p-12" |
| 135 | + handleEnter={handleSearchEnter} |
| 136 | + inputProps={{ |
| 137 | + placeholder: 'Search by repository or registry', |
| 138 | + autoFocus: true, |
| 139 | + }} |
| 140 | + /> |
| 141 | + |
| 142 | + {!!searchText && !filteredChartList.length ? ( |
| 143 | + <div ref={scrollableRef} className="flex-grow-1 flex"> |
| 144 | + <GenericEmptyState |
| 145 | + imgName="img-no-result" |
| 146 | + title={`No result for "${searchText}"`} |
| 147 | + subTitle={EMPTY_STATE_STATUS.CHART.NO_CHART_FOUND} |
| 148 | + imageType={ImageType.Medium} |
| 149 | + /> |
| 150 | + </div> |
| 151 | + ) : ( |
| 152 | + <div ref={scrollableRef} className="dc__overflow-auto flexbox-col flex-grow-1 mxh-350"> |
| 153 | + {filteredChartList.map( |
| 154 | + (list, index) => |
| 155 | + list.id !== 1 && ( |
| 156 | + <ChartListPopUpRow |
| 157 | + key={list.name} |
| 158 | + index={index} |
| 159 | + list={list} |
| 160 | + enabled={chartActiveMap[list.name]} |
| 161 | + toggleEnabled={toggleEnabled(list.name)} |
| 162 | + /> |
| 163 | + ), |
| 164 | + )} |
| 165 | + <div className="p-16" style={{ marginTop: 'auto' }}> |
| 166 | + <InfoBlock |
| 167 | + variant="help" |
| 168 | + description={ |
| 169 | + <div> |
| 170 | + <span> |
| 171 | + Showing Chart repositories and OCI Registries (used as chart repositories). |
| 172 | + You can add other |
| 173 | + </span> |
| 174 | + <NavLink to={URLS.GLOBAL_CONFIG_CHART}>Chart repositories</NavLink> |
| 175 | + <span> or </span> |
| 176 | + <NavLink to={URLS.GLOBAL_CONFIG_DOCKER}>OCI Registries</NavLink> |
| 177 | + <span> as chart sources.</span> |
| 178 | + </div> |
| 179 | + } |
| 180 | + /> |
| 181 | + </div> |
| 182 | + </div> |
| 183 | + )} |
| 184 | + </> |
| 185 | + ) |
| 186 | + } |
| 187 | + |
| 188 | + return ( |
| 189 | + <Popover |
| 190 | + open={open} |
| 191 | + overlayProps={overlayProps} |
| 192 | + popoverProps={popoverProps} |
| 193 | + triggerProps={triggerProps} |
| 194 | + triggerElement={null} |
| 195 | + buttonProps={{ |
| 196 | + onClick: handleSourceBtnClick, |
| 197 | + text: 'source', |
| 198 | + variant: ButtonVariantType.secondary, |
| 199 | + size: ComponentSizeType.xxs, |
| 200 | + dataTestId: 'chart-store-source-button', |
| 201 | + startIcon: <Icon name="ic-input" color={null} />, |
| 202 | + }} |
| 203 | + > |
| 204 | + <div className="charts-list flexbox-col mh-400"> |
| 205 | + <div className="flex dc__content-space px-16 pt-12 pb-11 border__primary--bottom"> |
| 206 | + <h4 className="m-0 fs-14 lh-1-5 fw-6">Helm chart sources</h4> |
| 207 | + <div className="flex dc__gap-12"> |
| 208 | + <AddChartSource text="Add" /> |
| 209 | + <Button |
| 210 | + isLoading={isFetching} |
| 211 | + icon={<Icon name="ic-arrows-clockwise" color={null} />} |
| 212 | + size={ComponentSizeType.xxs} |
| 213 | + variant={ButtonVariantType.borderLess} |
| 214 | + dataTestId="chart-store-refetch-button" |
| 215 | + ariaLabel="Refetch charts" |
| 216 | + onClick={handleRefetchCharts} |
| 217 | + /> |
| 218 | + <div className="divider__primary" /> |
| 219 | + <Button |
| 220 | + icon={<Icon name="ic-close-large" size={16} color={null} />} |
| 221 | + onClick={closePopover} |
| 222 | + size={ComponentSizeType.xxs} |
| 223 | + variant={ButtonVariantType.borderLess} |
| 224 | + dataTestId="chart-store-close-button" |
| 225 | + showAriaLabelInTippy={false} |
| 226 | + ariaLabel="Close" |
| 227 | + style={ButtonStyleType.negativeGrey} |
| 228 | + /> |
| 229 | + </div> |
| 230 | + </div> |
| 231 | + {renderChartListBody()} |
| 232 | + </div> |
| 233 | + </Popover> |
| 234 | + ) |
| 235 | +} |
0 commit comments