Skip to content

Commit 2ed1bcf

Browse files
Feat: use new tree listing (#1709)
* feat: use new treeListing - Replaces old treeListing page with new endpoint and status format - Reorganizes the routes and component files - Refactors some component parts in order to be reused between treeListings * feat: add treeListing banner Used as a link between the newer and older versions of the treeListing, should be removed after the new tree listing is stable * feat: add treeListing feature flag Adds a feature flag to select the version of the main treeListing page Closes #1559 * fix: show origins in any listing route
1 parent b8e4e57 commit 2ed1bcf

26 files changed

Lines changed: 1211 additions & 201 deletions

File tree

dashboard/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Use port 8000 to go directly to the backend/gunicorn.
22
# Use port 80 to go through Nginx (proxy service on Docker)
33
VITE_API_BASE_URL=http://localhost:8000
4+
5+
# Feature Flags
46
VITE_FEATURE_FLAG_SHOW_DEV=false
7+
VITE_FEATURE_FLAG_TREE_LISTING_VERSION=v1
8+
59
PLAYWRIGHT_TEST_BASE_URL=https://staging.dashboard.kernelci.org:9000

dashboard/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,8 @@ Also, we are using file based routing in the tanstack router, only files that st
7070
# Feature Flags
7171

7272
They are used when we want to hide a feature for some users, without having to do branch manipulation.
73-
Right now the only feature flag is for Dev only and it is controlled by the env
74-
`FEATURE_FLAG_SHOW_DEV=false` it is a boolean.
73+
74+
Available feature flags:
75+
76+
- `VITE_FEATURE_FLAG_SHOW_DEV` - Controls visibility of dev-only features (boolean, default: `false`)
77+
- `VITE_FEATURE_FLAG_TREE_LISTING_VERSION` - Controls which tree listing version to display. Set to `"v1"` for the old version or `"v2"` for the new version (string, default: `"v1"`)

dashboard/src/api/tree.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import type {
77
Tree,
88
TreeFastPathResponse,
99
TreeLatestResponse,
10+
TreeV2,
1011
} from '@/types/tree/Tree';
1112
import { DEFAULT_ORIGIN } from '@/types/general';
1213

14+
import type { TreeListingRoutesMap } from '@/utils/constants/treeListing';
15+
1316
import { RequestData } from './commonRequest';
1417

1518
const fetchTreeCheckoutData = async (
@@ -29,11 +32,12 @@ const fetchTreeCheckoutData = async (
2932

3033
export const useTreeTable = ({
3134
enabled,
35+
searchFrom,
3236
}: {
3337
enabled: boolean;
38+
searchFrom: TreeListingRoutesMap['v1']['search'];
3439
}): UseQueryResult<Tree[]> => {
35-
const { origin, intervalInDays } = useSearch({ from: '/_main/tree' });
36-
40+
const { origin, intervalInDays } = useSearch({ from: searchFrom });
3741
const queryKey = ['treeTable', origin, intervalInDays];
3842

3943
return useQuery({
@@ -46,7 +50,7 @@ export const useTreeTable = ({
4650

4751
const fetchTreeFastCheckoutData = async (
4852
origin: string,
49-
intervalInDays?: number,
53+
intervalInDays: number,
5054
): Promise<TreeFastPathResponse> => {
5155
const params = {
5256
origin: origin,
@@ -59,8 +63,12 @@ const fetchTreeFastCheckoutData = async (
5963
return data;
6064
};
6165

62-
export const useTreeTableFast = (): UseQueryResult<TreeFastPathResponse> => {
63-
const { origin, intervalInDays } = useSearch({ from: '/_main/tree' });
66+
export const useTreeTableFast = ({
67+
searchFrom,
68+
}: {
69+
searchFrom: TreeListingRoutesMap['v1']['search'];
70+
}): UseQueryResult<TreeFastPathResponse> => {
71+
const { origin, intervalInDays } = useSearch({ from: searchFrom });
6472

6573
const queryKey = ['treeTableFast', origin, intervalInDays];
6674

@@ -100,3 +108,33 @@ export const useTreeLatest = (
100108
refetchOnWindowFocus: false,
101109
});
102110
};
111+
112+
const fetchTreeListingV2 = async (
113+
origin: string,
114+
intervalInDays: number,
115+
): Promise<TreeV2[]> => {
116+
const params = {
117+
origin: origin,
118+
interval_in_days: intervalInDays,
119+
};
120+
121+
const data = await RequestData.get<TreeV2[]>('/api/tree-v2/', {
122+
params,
123+
});
124+
return data;
125+
};
126+
127+
export const useTreeListingV2 = ({
128+
searchFrom,
129+
}: {
130+
searchFrom: TreeListingRoutesMap['v2']['search'];
131+
}): UseQueryResult<TreeV2[]> => {
132+
const { origin, intervalInDays } = useSearch({ from: searchFrom });
133+
const queryKey = ['treeTableV2', origin, intervalInDays];
134+
135+
return useQuery({
136+
queryKey,
137+
queryFn: () => fetchTreeListingV2(origin, intervalInDays),
138+
refetchOnWindowFocus: false,
139+
});
140+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { VariantProps } from 'class-variance-authority';
2+
import { cva } from 'class-variance-authority';
3+
import type { JSX } from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
const bannerVariants = cva('rounded-md p-3 text-sm', {
8+
variants: {
9+
variant: {
10+
default:
11+
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
12+
green:
13+
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
14+
},
15+
},
16+
defaultVariants: {
17+
variant: 'default',
18+
},
19+
});
20+
21+
export const BaseBanner = ({
22+
children,
23+
variant,
24+
className,
25+
}: {
26+
children: JSX.Element;
27+
variant?: VariantProps<typeof bannerVariants>['variant'];
28+
className?: string;
29+
}): JSX.Element => {
30+
return (
31+
<div className={cn(bannerVariants({ variant }), className)}>{children}</div>
32+
);
33+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { LinkProps } from '@tanstack/react-router';
2+
import { Link } from '@tanstack/react-router';
3+
import type { JSX } from 'react';
4+
import type { MessageDescriptor } from 'react-intl';
5+
import { FormattedMessage, useIntl } from 'react-intl';
6+
7+
import { BaseBanner } from '@/components/Banner/BaseBanner';
8+
9+
const GITHUB_ISSUES_URL = 'https://github.com/kernelci/dashboard/issues';
10+
11+
interface IPageBanner {
12+
pageNameId: MessageDescriptor['id'];
13+
pageRoute: LinkProps['to'];
14+
}
15+
16+
/**
17+
* This is the banner that goes in the older page
18+
*/
19+
export const OldPageBanner = ({
20+
pageNameId,
21+
pageRoute: newerPageRoute,
22+
}: IPageBanner): JSX.Element => {
23+
const { formatMessage } = useIntl();
24+
25+
return (
26+
<BaseBanner>
27+
<FormattedMessage
28+
id="messages.olderPageVersion"
29+
values={{
30+
page: formatMessage({ id: pageNameId }),
31+
gitHubLink: (
32+
<a
33+
href={GITHUB_ISSUES_URL}
34+
target="_blank"
35+
rel="noreferrer"
36+
className="underline"
37+
>
38+
{formatMessage({ id: 'global.gitHubIssue' })}
39+
</a>
40+
),
41+
newPageLink: (
42+
<Link to={newerPageRoute} className="underline">
43+
{formatMessage({ id: 'global.here' })}
44+
</Link>
45+
),
46+
}}
47+
/>
48+
</BaseBanner>
49+
);
50+
};
51+
52+
/**
53+
* This is the banner that goes in the newer page
54+
*/
55+
export const NewPageBanner = ({
56+
pageNameId,
57+
pageRoute: olderPageRoute,
58+
}: IPageBanner): JSX.Element => {
59+
const { formatMessage } = useIntl();
60+
61+
return (
62+
<BaseBanner variant="green">
63+
<FormattedMessage
64+
id="messages.newerPageVersion"
65+
values={{
66+
page: formatMessage({ id: pageNameId }),
67+
gitHubLink: (
68+
<a
69+
href={GITHUB_ISSUES_URL}
70+
target="_blank"
71+
rel="noreferrer"
72+
className="underline"
73+
>
74+
{formatMessage({ id: 'global.gitHubIssue' })}
75+
</a>
76+
),
77+
oldVersionLink: (
78+
<Link to={olderPageRoute} className="underline">
79+
{formatMessage({ id: 'global.here' })}
80+
</Link>
81+
),
82+
}}
83+
/>
84+
</BaseBanner>
85+
);
86+
};

dashboard/src/components/OpenGraphTags/ListingOGTags.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { memo, useMemo } from 'react';
33

44
import { useIntl } from 'react-intl';
55

6-
import type { PossibleMonitorPath } from '@/types/general';
6+
import type { ListingPaths } from '@/types/general';
77
import type { MessagesKey } from '@/locales/messages';
88

99
import { OpenGraphTags } from './OpenGraphTags';
@@ -12,7 +12,7 @@ const ListingOGTags = ({
1212
monitor,
1313
search,
1414
}: {
15-
monitor: PossibleMonitorPath;
15+
monitor: ListingPaths;
1616
search: string;
1717
}): JSX.Element => {
1818
const { formatMessage } = useIntl();
@@ -30,9 +30,6 @@ const ListingOGTags = ({
3030
case '/issues':
3131
descriptionId = 'issueListing.description';
3232
break;
33-
case '/hardware-new':
34-
descriptionId = 'hardwareListing.description';
35-
break;
3633
}
3734
return (
3835
formatMessage({ id: descriptionId }) +
@@ -50,8 +47,6 @@ const ListingOGTags = ({
5047
return formatMessage({ id: 'hardwareListing.title' });
5148
case '/issues':
5249
return formatMessage({ id: 'issueListing.title' });
53-
case '/hardware-new':
54-
return formatMessage({ id: 'hardwareListing.title' });
5550
}
5651
}, [formatMessage, monitor]);
5752

dashboard/src/components/Status/Status.tsx

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import ColoredCircle from '@/components/ColoredCircle/ColoredCircle';
66
import type { GroupedStatus } from '@/utils/status';
77
import { groupStatus } from '@/utils/status';
88

9+
// TODO: replace the preCalculatedGroupedStatus with a type-safe approach,
10+
// currently everything is optional
911
interface ITestStatus {
1012
pass?: number;
1113
error?: number;
@@ -64,40 +66,30 @@ export const GroupedTestStatus = ({
6466
);
6567
};
6668

67-
interface ITestStatusWithLink extends ITestStatus {
69+
interface IStatusLinkProps {
6870
passLinkProps?: LinkProps;
6971
failLinkProps?: LinkProps;
7072
inconclusiveLinkProps?: LinkProps;
7173
}
7274

73-
export const GroupedTestStatusWithLink = ({
74-
pass,
75-
error,
76-
miss,
77-
fail,
78-
done,
79-
skip,
80-
nullStatus,
81-
hideInconclusive = false,
75+
interface IBaseGroupedStatusWithLink extends IStatusLinkProps {
76+
groupedStatus: GroupedStatus;
77+
hideInconclusive?: boolean;
78+
}
79+
80+
export const BaseGroupedStatusWithLink = ({
81+
groupedStatus,
8282
passLinkProps,
8383
failLinkProps,
8484
inconclusiveLinkProps,
85-
}: ITestStatusWithLink): JSX.Element => {
86-
const { successCount, inconclusiveCount, failedCount } = groupStatus({
87-
doneCount: done,
88-
errorCount: error,
89-
failCount: fail,
90-
missCount: miss,
91-
passCount: pass,
92-
skipCount: skip,
93-
nullCount: nullStatus,
94-
});
85+
hideInconclusive = false,
86+
}: IBaseGroupedStatusWithLink): JSX.Element => {
9587
return (
9688
<div className="flex flex-row gap-1">
9789
{
9890
<Link {...passLinkProps}>
9991
<ColoredCircle
100-
quantity={successCount ?? 0}
92+
quantity={groupedStatus.successCount}
10193
tooltipText="global.success"
10294
backgroundClassName="bg-light-green"
10395
/>
@@ -106,7 +98,7 @@ export const GroupedTestStatusWithLink = ({
10698
{
10799
<Link {...failLinkProps}>
108100
<ColoredCircle
109-
quantity={failedCount}
101+
quantity={groupedStatus.failedCount}
110102
tooltipText="global.failed"
111103
backgroundClassName="bg-light-red"
112104
/>
@@ -115,7 +107,7 @@ export const GroupedTestStatusWithLink = ({
115107
{!hideInconclusive && (
116108
<Link {...inconclusiveLinkProps}>
117109
<ColoredCircle
118-
quantity={inconclusiveCount ?? 0}
110+
quantity={groupedStatus.inconclusiveCount}
119111
tooltipText="global.inconclusive"
120112
backgroundClassName="bg-medium-gray"
121113
/>
@@ -125,6 +117,42 @@ export const GroupedTestStatusWithLink = ({
125117
);
126118
};
127119

120+
interface ITestStatusWithLink extends ITestStatus, IStatusLinkProps {}
121+
122+
export const GroupedTestStatusWithLink = ({
123+
pass,
124+
error,
125+
miss,
126+
fail,
127+
done,
128+
skip,
129+
nullStatus,
130+
hideInconclusive = false,
131+
passLinkProps,
132+
failLinkProps,
133+
inconclusiveLinkProps,
134+
}: ITestStatusWithLink): JSX.Element => {
135+
const groupedStatus = groupStatus({
136+
doneCount: done,
137+
errorCount: error,
138+
failCount: fail,
139+
missCount: miss,
140+
passCount: pass,
141+
skipCount: skip,
142+
nullCount: nullStatus,
143+
});
144+
145+
return (
146+
<BaseGroupedStatusWithLink
147+
groupedStatus={groupedStatus}
148+
passLinkProps={passLinkProps}
149+
failLinkProps={failLinkProps}
150+
inconclusiveLinkProps={inconclusiveLinkProps}
151+
hideInconclusive={hideInconclusive}
152+
/>
153+
);
154+
};
155+
128156
interface IBuildStatus {
129157
valid?: number;
130158
invalid?: number;

0 commit comments

Comments
 (0)