Skip to content

Commit 860b404

Browse files
authored
Merge pull request #813 from amitamrutiya/performer-section
Convert cloud performance section into sistent component
2 parents 51bd41a + 411a49f commit 860b404

File tree

10 files changed

+672
-5
lines changed

10 files changed

+672
-5
lines changed

src/custom/CustomCatalog/style.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,15 +348,16 @@ export const CardBack = styled('div')<CatalogProps>(({ isCatalog }) => ({
348348
})
349349
}));
350350

351-
const getBackground = (isLightMode: boolean) => {
351+
export const getCatalogCardBackground = (isLightMode: boolean) => {
352352
const lightGradient = `linear-gradient(to left bottom, ${WHITESMOKE}, ${GRAY97},white, white, white, white, white, white, white, white, ${WHITESMOKE}, ${GRAY97})`;
353353
const darkGradient = `linear-gradient(to right top, ${DARK_PRIMARY_COLOR}, ${accentGrey[30]}, ${accentGrey[20]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${charcoal[20]}, ${charcoal[10]}, black)`;
354354

355355
return isLightMode ? lightGradient : darkGradient;
356356
};
357+
357358
export const CardFront = styled('div')<DesignCardDivProps>(({ shouldFlip, isDetailed, theme }) => {
358359
const isLightMode = theme.palette.mode === 'light';
359-
const background = getBackground(isLightMode);
360+
const background = getCatalogCardBackground(isLightMode);
360361
const boxShadow = `2px 2px 3px 0px ${theme.palette.background.brand?.default}`;
361362

362363
return {
@@ -414,7 +415,7 @@ export const DesignAuthorName = styled('div')(() => ({
414415

415416
export const CatalogEmptyStateDiv = styled('div')(({ theme }) => {
416417
const isLightMode = theme.palette.mode === 'light';
417-
const background = getBackground(isLightMode);
418+
const background = getCatalogCardBackground(isLightMode);
418419
const boxShadow = `2px 2px 3px 0px ${theme.palette.background.brand?.default}`;
419420

420421
return {
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { memo, useMemo } from 'react';
3+
import {
4+
CloneIcon,
5+
DeploymentsIcon,
6+
DownloadIcon,
7+
OpenIcon,
8+
ShareIcon,
9+
TropyIcon
10+
} from '../../icons';
11+
import { useTheme } from '../../theme';
12+
import { Pattern } from '../CustomCatalog/CustomCard';
13+
import { ErrorBoundary } from '../ErrorBoundary';
14+
import { StateCardSekeleton } from './PerformersToogleButton';
15+
import {
16+
CardsContainer,
17+
ContentWrapper,
18+
ErrorContainer,
19+
HeaderSection,
20+
HeaderTitle,
21+
IconContainer,
22+
MainContainer,
23+
RepoSection,
24+
RepoTitle,
25+
StatsValue,
26+
StatusLabel,
27+
StyledCard,
28+
Title,
29+
UserNameText
30+
} from './styles';
31+
32+
interface MetricConfig {
33+
label: string;
34+
icon: React.ComponentType<any>;
35+
id: string;
36+
countKey: keyof Pattern;
37+
}
38+
39+
interface BaseQueryParams {
40+
pathType: string;
41+
page: number;
42+
pagesize: number;
43+
metrics: boolean;
44+
expandUser: boolean;
45+
trim: boolean;
46+
order?: string;
47+
}
48+
49+
interface StatCardProps {
50+
label: string;
51+
count: number;
52+
patternName: string;
53+
pattern: Pattern;
54+
userName: string;
55+
userid: string;
56+
icon: React.ComponentType<any>;
57+
status: string;
58+
id: string;
59+
onCardClick: (pattern: Pattern) => void;
60+
onIconClick: () => void;
61+
onAuthorClick: (userId: string) => void;
62+
onStatusClick: (status: string) => void;
63+
}
64+
65+
interface PerformersSectionProps {
66+
useGetCatalogFilters: (params: any) => any;
67+
onCardClick: (pattern: Pattern) => void;
68+
onIconClick: () => void;
69+
onAuthorClick: (userId: string) => void;
70+
onStatusClick: (status: string) => void;
71+
}
72+
73+
type MetricType = 'view' | 'clone' | 'download' | 'deployment' | 'share';
74+
75+
const BASE_QUERY_PARAMS: BaseQueryParams = {
76+
pathType: 'pattern',
77+
page: 0,
78+
pagesize: 1,
79+
metrics: true,
80+
expandUser: true,
81+
trim: true
82+
};
83+
84+
const METRICS: Record<MetricType, MetricConfig> = {
85+
view: {
86+
label: 'Most Opens',
87+
icon: OpenIcon,
88+
id: 'open-icon',
89+
countKey: 'view_count'
90+
},
91+
clone: {
92+
label: 'Most Clones',
93+
icon: CloneIcon,
94+
id: 'clone-icon',
95+
countKey: 'clone_count'
96+
},
97+
download: {
98+
label: 'Most Downloads',
99+
icon: DownloadIcon,
100+
id: 'download-icon',
101+
countKey: 'download_count'
102+
},
103+
deployment: {
104+
label: 'Most Deploys',
105+
icon: DeploymentsIcon,
106+
id: 'deployments-icon',
107+
countKey: 'deployment_count'
108+
},
109+
share: {
110+
label: 'Most Shares',
111+
icon: ShareIcon,
112+
id: 'share-icon',
113+
countKey: 'share_count'
114+
}
115+
};
116+
117+
const createQueryParams = (metric: MetricType): BaseQueryParams => ({
118+
...BASE_QUERY_PARAMS,
119+
order: `${METRICS[metric].countKey} desc`
120+
});
121+
122+
const StatCardComponent: React.FC<StatCardProps> = ({
123+
label,
124+
count,
125+
patternName,
126+
pattern,
127+
userName,
128+
userid,
129+
icon: Icon,
130+
status,
131+
id,
132+
onCardClick,
133+
onIconClick,
134+
onAuthorClick,
135+
onStatusClick
136+
}) => {
137+
const handleCardClick = () => {
138+
onCardClick(pattern);
139+
};
140+
141+
const handleIconClick = (e: React.MouseEvent) => {
142+
e.stopPropagation();
143+
onIconClick();
144+
};
145+
146+
const handleAuthorClick = (e: React.MouseEvent) => {
147+
e.stopPropagation();
148+
onAuthorClick(userid);
149+
};
150+
151+
const handleStatusClick = (e: React.MouseEvent) => {
152+
e.stopPropagation();
153+
onStatusClick(status);
154+
};
155+
156+
return (
157+
<StyledCard elevation={0} status={status} onClick={handleCardClick}>
158+
<ContentWrapper cardId={id}>
159+
<HeaderSection>
160+
<HeaderTitle>{label}</HeaderTitle>
161+
<IconContainer onClick={handleIconClick}>
162+
<Icon className={id} />
163+
</IconContainer>
164+
</HeaderSection>
165+
166+
<StatsValue>{count}</StatsValue>
167+
168+
<RepoSection>
169+
<RepoTitle>{patternName}</RepoTitle>
170+
<UserNameText onClick={handleAuthorClick}>by {userName}</UserNameText>
171+
</RepoSection>
172+
</ContentWrapper>
173+
<StatusLabel labelType={status} onClick={handleStatusClick}>
174+
{status}
175+
</StatusLabel>
176+
</StyledCard>
177+
);
178+
};
179+
interface PageArgs {
180+
search?: string;
181+
order?: string;
182+
pagesize?: number;
183+
page?: number;
184+
[key: string]: any;
185+
}
186+
187+
const withDefaultPageArgs = (args: PageArgs = {}): PageArgs => ({
188+
search: args.search ?? '',
189+
order: args.order ?? '',
190+
pagesize: args.pagesize ?? 0,
191+
page: args.page ?? 0,
192+
...args
193+
});
194+
195+
const StatCard = memo(StatCardComponent);
196+
StatCard.displayName = 'StatCard';
197+
198+
const useMetricQueries = (useGetCatalogFilters: PerformersSectionProps['useGetCatalogFilters']) => {
199+
const viewQuery = useGetCatalogFilters(withDefaultPageArgs(createQueryParams('view')));
200+
201+
const cloneQuery = useGetCatalogFilters(withDefaultPageArgs(createQueryParams('clone')));
202+
203+
const downloadQuery = useGetCatalogFilters(withDefaultPageArgs(createQueryParams('download')));
204+
205+
const deploymentQuery = useGetCatalogFilters(
206+
withDefaultPageArgs(createQueryParams('deployment'))
207+
);
208+
209+
const shareQuery = useGetCatalogFilters(withDefaultPageArgs(createQueryParams('share')));
210+
211+
const metricQueries = {
212+
view: viewQuery,
213+
clone: cloneQuery,
214+
download: downloadQuery,
215+
deployment: deploymentQuery,
216+
share: shareQuery
217+
};
218+
219+
return {
220+
queries: metricQueries,
221+
isLoading: Object.values(metricQueries).some((query) => query.isLoading),
222+
hasError: Object.values(metricQueries).some((query) => query.isError)
223+
};
224+
};
225+
226+
const processQueryData = (
227+
queries: Record<MetricType, any>,
228+
metric: MetricType
229+
): Omit<
230+
StatCardProps,
231+
'onCardClick' | 'onIconClick' | 'onAuthorClick' | 'onStatusClick'
232+
> | null => {
233+
const query = queries[metric];
234+
const config = METRICS[metric];
235+
const pattern = query?.isSuccess && query.data?.patterns?.[0];
236+
237+
if (!pattern) return null;
238+
239+
return {
240+
label: config.label,
241+
count: pattern[config.countKey],
242+
patternName: pattern.name || 'Unknown',
243+
pattern: pattern,
244+
userName: pattern.user?.first_name || 'Unknown',
245+
userid: pattern.user?.id,
246+
icon: config.icon,
247+
id: config.id,
248+
status: pattern?.catalog_data?.content_class
249+
};
250+
};
251+
252+
const PerformersSection: React.FC<PerformersSectionProps> = ({
253+
useGetCatalogFilters,
254+
onCardClick,
255+
onIconClick,
256+
onAuthorClick,
257+
onStatusClick
258+
}) => {
259+
const theme = useTheme();
260+
const { queries, isLoading, hasError } = useMetricQueries(useGetCatalogFilters);
261+
262+
const stats = useMemo(
263+
() =>
264+
(Object.keys(METRICS) as MetricType[])
265+
.map((metric) => processQueryData(queries, metric))
266+
.filter(
267+
(
268+
stat
269+
): stat is Omit<
270+
StatCardProps,
271+
'onCardClick' | 'onIconClick' | 'onAuthorClick' | 'onStatusClick'
272+
> => Boolean(stat)
273+
),
274+
[queries]
275+
);
276+
277+
if (hasError)
278+
return (
279+
<MainContainer>
280+
<ErrorContainer>Error loading statistics. Please try again later.</ErrorContainer>
281+
</MainContainer>
282+
);
283+
284+
return (
285+
<ErrorBoundary>
286+
<MainContainer>
287+
<Title>
288+
Top Performers
289+
<TropyIcon
290+
style={{
291+
height: '2rem',
292+
width: '2rem',
293+
color: theme.palette.icon.secondary
294+
}}
295+
/>
296+
</Title>
297+
<CardsContainer>
298+
{isLoading && <StateCardSekeleton />}
299+
{!isLoading &&
300+
stats.map((stat, index) => (
301+
<StatCard
302+
key={`${stat.id}-${index}`}
303+
{...stat}
304+
onCardClick={onCardClick}
305+
onIconClick={onIconClick}
306+
onAuthorClick={onAuthorClick}
307+
onStatusClick={onStatusClick}
308+
/>
309+
))}
310+
</CardsContainer>
311+
</MainContainer>
312+
</ErrorBoundary>
313+
);
314+
};
315+
316+
export default memo(PerformersSection);

0 commit comments

Comments
 (0)