Skip to content

Commit

Permalink
Merge branch 'master' into snyk-fix-30b0e17215b00fc723033cf1d50d04bc
Browse files Browse the repository at this point in the history
  • Loading branch information
emirgens authored Sep 10, 2024
2 parents 505566c + 848f214 commit 39ffd87
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 80 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ lint:
npm run "lint"
npm run "lint-ts"

.PHONY: lint-fix
lint-fix:
npm run "lint-fix"

.PHONY: lint-strict
lint-strict:
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"src/style/hex-to-hsl.js",
"build/",
"dist/",
"src/store/*.ts"
"src/store/*.ts",
".idea"
]
},
"formatter": {
Expand Down
4 changes: 2 additions & 2 deletions radixconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ spec:
- name: http
port: 8080
runtime:
architecture: amd64
architecture: arm64
variables:
CLUSTER_EGRESS_IPS: undefined # Comma separated list of IPs of all Public IP Prefixes assigned to the ClusterType (production, playground, development)
CLUSTER_INGRESS_IPS: undefined # Comma separated list of IPs of all Public IP Prefixes assigned to the ClusterType (production, playground, development)
Expand Down Expand Up @@ -80,7 +80,7 @@ spec:
port: 8000
publicPort: http
runtime:
architecture: amd64
architecture: arm64
secrets:
- OAUTH2_PROXY_CLIENT_ID # ID of the "Web Console" AD app. This is a secret so it can be configured per cluster, but it's not sensitive info
- OAUTH2_PROXY_CLIENT_SECRET # Azure client secret for "Web Console frontend app" in the "Web Console" AD app for the appropriate cluster
Expand Down
8 changes: 8 additions & 0 deletions src/components/app-list-item/dev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const testData: Array<{ description: string } & AppListItemProps> = [
description: 'App',
app: { name: 'test-app' },
handler: noop,
isLoaded: true,
name: 'some-app',
},
{
description: 'App, marked Favourite, with Job',
Expand All @@ -29,19 +31,25 @@ const testData: Array<{ description: string } & AppListItemProps> = [
handler: noop,
isFavourite: true,
showStatus: true,
isLoaded: true,
name: 'some-app',
},
{
description: 'App, marked Favourite, without Job',
app: { name: 'app-test' },
handler: noop,
isFavourite: true,
showStatus: true,
isLoaded: true,
name: 'some-app',
},
{
description: 'App, marked Placeholder',
app: { name: 'app-placeholder' },
handler: noop,
isPlaceholder: true,
isLoaded: true,
name: 'some-app',
},
];

Expand Down
31 changes: 23 additions & 8 deletions src/components/app-list-item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {
Button,
CircularProgress,
Icon,
Tooltip,
Typography,
} from '@equinor/eds-core-react';
import { star_filled, star_outlined } from '@equinor/eds-icons';
import { error_outlined, star_filled, star_outlined } from '@equinor/eds-icons';
import { clsx } from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import * as PropTypes from 'prop-types';
Expand Down Expand Up @@ -51,11 +52,13 @@ export type FavouriteClickedHandler = (
) => void;

export interface AppListItemProps {
app: Readonly<ApplicationSummary>;
app?: Readonly<ApplicationSummary>;
handler: FavouriteClickedHandler;
isPlaceholder?: boolean;
isFavourite?: boolean;
showStatus?: boolean;
name: string;
isLoaded: boolean;
}

const latestJobStatus: Partial<
Expand Down Expand Up @@ -193,39 +196,51 @@ export const AppListItem: FunctionComponent<AppListItemProps> = ({
isPlaceholder,
isFavourite,
showStatus,
name,
isLoaded,
}) => (
<WElement
className={clsx('app-list-item', {
'app-list-item--placeholder': isPlaceholder,
})}
appName={app.name}
appName={name}
isPlaceholder={isPlaceholder}
>
<div className="app-list-item--area">
<div className="app-list-item--area-icon">
<AppBadge appName={app.name} size={40} />
<AppBadge appName={name} size={40} />
</div>
<div className="grid app-list-item--area-details">
<div className="app-list-item--details">
<Typography className="app-list-item--details-title" variant="h6">
{app.name}
{name}
</Typography>
<div className="app-list-item--details-favourite">
<Button variant="ghost_icon" onClick={(e) => handler(e, app.name)}>
<Button variant="ghost_icon" onClick={(e) => handler(e, name)}>
<Icon data={isFavourite ? star_filled : star_outlined} />
</Button>
</div>
</div>
{showStatus && <AppItemStatus {...app} />}
{isLoaded &&
showStatus &&
(app ? (
<AppItemStatus {...app} />
) : (
<Tooltip title="This application does not exist">
<Icon data={error_outlined} />
</Tooltip>
))}
</div>
</div>
</WElement>
);

AppListItem.propTypes = {
app: PropTypes.object.isRequired as PropTypes.Validator<ApplicationSummary>,
app: PropTypes.object as PropTypes.Validator<ApplicationSummary>,
handler: PropTypes.func.isRequired,
isPlaceholder: PropTypes.bool,
isFavourite: PropTypes.bool,
showStatus: PropTypes.bool,
isLoaded: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
};
128 changes: 68 additions & 60 deletions src/components/app-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Button, CircularProgress, Typography } from '@equinor/eds-core-react';
import {
Button,
CircularProgress,
Icon,
Typography,
} from '@equinor/eds-core-react';
import { type FunctionComponent, useEffect, useState } from 'react';

import { radixApi, useGetSearchApplicationsQuery } from '../../store/radix-api';
Expand All @@ -8,6 +13,7 @@ import AsyncResource from '../async-resource/async-resource';
import PageCreateApplication from '../page-create-application';

import './style.css';
import { refresh } from '@equinor/eds-icons';
import { isEqual, uniq } from 'lodash';
import useLocalStorage from '../../effects/use-local-storage';
import { pollingInterval } from '../../store/defaults';
Expand All @@ -23,22 +29,34 @@ const LoadingCards: FunctionComponent<{ amount: number }> = ({ amount }) => (
app={{ name: 'dummy' }}
handler={(e) => e.preventDefault()}
isPlaceholder
name={''}
isLoaded={false}
/>
))}
</div>
);

const isArrayOfStrings = (variable: unknown): variable is string[] => {
return (
Array.isArray(variable) &&
variable.every((item) => typeof item === 'string')
);
};

export default function AppList() {
const [randomPlaceholderCount] = useState(Math.floor(Math.random() * 5) + 3);

const [favourites, setFavourites] = useLocalStorage<Array<string>>(
'favouriteApplications',
[]
[],
isArrayOfStrings
);

const [knownAppNames, setKnownAppNames] = useLocalStorage<Array<string>>(
'knownApplications',
[]
[],
isArrayOfStrings
);

const [refreshApps, appsState] =
radixApi.endpoints.showApplications.useLazyQuery({});

Expand All @@ -48,53 +66,50 @@ export default function AppList() {
includeEnvironmentActiveComponents: 'true',
includeLatestJobSummary: 'true',
},
{ skip: favourites.length === 0, pollingInterval }
{ skip: favourites?.length === 0, pollingInterval }
);

const changeFavouriteApplication = (app: string, isFavourite: boolean) => {
const changeFavouriteApplication = (
appName: string,
isFavourite: boolean
) => {
if (!favourites) {
setFavourites([appName]);
return;
}
if (isFavourite) {
setFavourites((old) => uniq([...old, app]));
} else {
setFavourites((old) => old.filter((a) => a !== app));
setFavourites((old) => uniq([...old, appName]));
return;
}
setFavourites((old) => old.filter((a) => a !== appName));
};

const knownApps = dataSorter(knownAppNames ?? [], [
(x, y) => sortCompareString(x, y),
]).map((appName) => ({
name: appName,
isFavourite: favourites.includes(appName),
isFavourite: favourites?.includes(appName),
}));

const favouriteApps = dataSorter(
[
...(favsData ?? [])
.filter(({ name }) => favourites.includes(name))
.map((favApp) => ({ isFavourite: true, ...favApp }) as const),
...knownApps,
].filter(
(app, i, arr) =>
app.isFavourite && arr.findIndex((x) => x.name === app.name) === i // remove non-favourites and duplicates
),
[(x, y) => sortCompareString(x.name, y.name)]
);
const favouriteNames = dataSorter(favourites ?? [], [
(x, y) => sortCompareString(x, y),
]);

// remove from know app names previously favorite knownApps, which do not currently exist
useEffect(() => {
if (!favsData || !knownApps) {
if (!favourites || !knownApps) {
return;
}
const knownAppNames = knownApps
.filter(
(knownApp) =>
!knownApp.isFavourite ||
favsData.some((favApp) => favApp.name === knownApp.name)
favourites.some((favAppName) => favAppName === knownApp.name)
)
.map((app) => app.name);
const favAppNames = favsData.map((app) => app.name);
const mergedKnownAndFavoriteAppNames = uniq([
...knownAppNames,
...favAppNames,
...favourites,
]).sort();
if (
!isEqual(
Expand All @@ -104,46 +119,34 @@ export default function AppList() {
) {
setKnownAppNames(mergedKnownAndFavoriteAppNames);
}
}, [knownApps, favsData, setKnownAppNames]);
}, [knownApps, favourites, setKnownAppNames]);

return (
<article className="grid grid--gap-medium">
<div className="app-list__header">
<Typography variant="body_short_bold">Favourites</Typography>
<div className="create-app">
<PageCreateApplication />
</div>
<PageCreateApplication />
</div>
<div className="app-list">
{favsState.isLoading ? (
<div>
<CircularProgress size={16} /> Loading favorites…
</div>
) : favouriteApps.length > 0 ? (
{favouriteNames?.length > 0 ? (
<>
<div className="grid grid--gap-medium app-list--section">
<AsyncResource
asyncState={{
...favsState,
isLoading: favsState.isLoading && !(favouriteApps.length > 0),
}}
loadingContent={<LoadingCards amount={favourites.length} />}
>
<div className="app-list__list">
{favouriteApps.map((app, i) => (
<AppListItem
key={i}
app={app}
handler={(e) => {
changeFavouriteApplication(app.name, false);
e.preventDefault();
}}
isFavourite
showStatus
/>
))}
</div>
</AsyncResource>
<div className="app-list__list">
{favouriteNames.map((appName) => (
<AppListItem
key={appName}
app={favsData?.find((a) => a.name === appName)}
handler={(e) => {
changeFavouriteApplication(appName, false);
e.preventDefault();
}}
isFavourite
showStatus
isLoaded={favsState.isSuccess}
name={appName}
/>
))}
</div>
</div>
</>
) : (
Expand All @@ -159,6 +162,8 @@ export default function AppList() {
)}
<Button
className={'action--justify-end'}
variant="ghost"
color="primary"
disabled={appsState.isLoading || appsState.isFetching}
onClick={() =>
promiseHandler(
Expand All @@ -168,6 +173,7 @@ export default function AppList() {
)
}
>
<Icon data={refresh} />
Refresh list
</Button>
</div>
Expand All @@ -180,7 +186,7 @@ export default function AppList() {
</div>
)}
<div className="grid grid--gap-medium app-list--section">
{(knownAppNames && knownAppNames.length > 0) ||
{knownAppNames?.length > 0 ||
appsState.isLoading ||
appsState.isFetching ? (
<AsyncResource
Expand All @@ -190,15 +196,17 @@ export default function AppList() {
}
>
<div className="app-list__list">
{knownApps.map((app, i) => (
{knownApps.map((app) => (
<AppListItem
key={i}
key={app.name}
app={app}
handler={(e) => {
changeFavouriteApplication(app.name, !app.isFavourite);
e.preventDefault();
}}
isFavourite={app.isFavourite}
name={app.name}
isLoaded={true}
/>
))}
</div>
Expand Down
Loading

0 comments on commit 39ffd87

Please sign in to comment.