Skip to content

Commit 0902202

Browse files
authored
Pull request update/250211
26f798a OSN-577. Introduce OPTSCALE_MODE query parameter and enhance component handling 69a0e4f OSN-576. Enhance Apollo error handling and improve user navigation 4eabf23 OSN-619. Add max spot cost per hour configuration for runset ed0bdb9 OSN-620. Refactor documentation URLs for data source connections be9641f OSN-585. Add tooltip for long names in ExpensesBreakdownTable
2 parents af17052 + 26f798a commit 0902202

File tree

28 files changed

+699
-277
lines changed

28 files changed

+699
-277
lines changed

ngui/ui/src/components/ApolloApiErrorAlert/ApolloApiErrorAlert.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { GET_ERROR } from "graphql/api/common";
88
const ApolloApiErrorAlert = () => {
99
const { data = {} } = useQuery(GET_ERROR);
1010

11-
const { error: { id, error_code: errorCode, reason: errorReason, url, params } = {} } = data;
11+
const { error: { id, error_code: errorCode, reason: errorReason, url, params, apolloErrorMessage } = {} } = data;
1212

1313
const [open, setOpen] = useState(false);
1414

@@ -23,7 +23,17 @@ const ApolloApiErrorAlert = () => {
2323
setOpen(false);
2424
};
2525

26-
const errorMessage = errorCode && <ApiErrorMessage errorCode={errorCode} reason={errorReason} url={url} params={params} />;
26+
const getErrorMessage = () => {
27+
if (errorCode) {
28+
return <ApiErrorMessage errorCode={errorCode} reason={errorReason} url={url} params={params} />;
29+
}
30+
if (apolloErrorMessage) {
31+
return apolloErrorMessage;
32+
}
33+
return null;
34+
};
35+
36+
const errorMessage = getErrorMessage();
2737

2838
return (
2939
errorMessage !== null && (

ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@ import { createClient } from "graphql-ws";
77
import { v4 as uuidv4 } from "uuid";
88
import { GET_ERROR } from "graphql/api/common";
99
import { useGetToken } from "hooks/useGetToken";
10+
import { useSignOut } from "hooks/useSignOut";
1011
import { getEnvironmentVariable } from "utils/env";
1112

1213
const httpBase = getEnvironmentVariable("VITE_APOLLO_HTTP_BASE");
1314
const wsBase = getEnvironmentVariable("VITE_APOLLO_WS_BASE");
1415

1516
const writeErrorToCache = (cache: DefaultContext, graphQLError: GraphQLError) => {
16-
const { extensions: { response: { url, body: { error } = {} } = {} } = {} } = graphQLError;
17+
const { extensions: { response: { url, body: { error } = {} } = {} } = {}, message } = graphQLError;
1718

1819
cache.writeQuery({
1920
query: GET_ERROR,
20-
data: { error: { __typename: "Error", id: uuidv4(), ...error, url } }
21+
data: { error: { __typename: "Error", id: uuidv4(), ...error, apolloErrorMessage: message, url } }
2122
});
2223
};
2324

2425
const ApolloClientProvider = ({ children }) => {
2526
const { token } = useGetToken();
2627

28+
const signOut = useSignOut();
29+
2730
const httpLink = new HttpLink({
2831
uri: `${httpBase}/api`,
2932
headers: {
@@ -39,7 +42,15 @@ const ApolloClientProvider = ({ children }) => {
3942

4043
const errorLink = onError(({ graphQLErrors, networkError, operation }: ErrorResponse) => {
4144
if (graphQLErrors) {
42-
graphQLErrors.forEach(({ message, path }) => console.log(`[GraphQL error]: Message: ${message}, Path: ${path}`));
45+
graphQLErrors.forEach((graphQLError) => {
46+
const { message, path, extensions } = graphQLError;
47+
48+
console.log(`[GraphQL error]: Message: ${message}, Path: ${path}`);
49+
50+
if (extensions?.response?.status === 401) {
51+
signOut();
52+
}
53+
});
4354

4455
const { cache } = operation.getContext();
4556
writeErrorToCache(cache, graphQLErrors[0]);

ngui/ui/src/components/App/App.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import ErrorBoundary from "components/ErrorBoundary";
33
import LayoutWrapper from "components/LayoutWrapper";
44
import RoutePathContextProvider from "contexts/RoutePathContext/RoutePathContextProvider";
55
import { useGetToken } from "hooks/useGetToken";
6-
import { LOGIN, USER_EMAIL_QUERY_PARAMETER_NAME } from "urls";
6+
import {
7+
INITIALIZE,
8+
LOGIN,
9+
NEXT_QUERY_PARAMETER_NAME,
10+
OPTSCALE_MODE_QUERY_PARAMETER_NAME,
11+
USER_EMAIL_QUERY_PARAMETER_NAME
12+
} from "urls";
713
import mainMenu from "utils/menus";
814
import { formQueryString, getPathname, getQueryParams } from "utils/network";
9-
import { isEmpty } from "utils/objects";
15+
import { isEmpty as isEmptyObject } from "utils/objects";
1016
import { routes } from "utils/routes";
1117

1218
const RouteContent = ({ component, layout, context }) => (
@@ -17,22 +23,33 @@ const LoginNavigation = () => {
1723
const currentPathName = getPathname();
1824
const currentQueryParams = getQueryParams();
1925

20-
const { [USER_EMAIL_QUERY_PARAMETER_NAME]: email, ...restQueryParams } = currentQueryParams;
21-
22-
const getNextParameter = () => {
23-
const nextRoute = currentPathName;
24-
const nextRouteQueryParams = isEmpty(restQueryParams) ? "" : `?${formQueryString(restQueryParams).replace(/&/g, "%26")}`;
25-
26-
return `next=${nextRoute}${nextRouteQueryParams}`;
26+
const {
27+
[USER_EMAIL_QUERY_PARAMETER_NAME]: email,
28+
[OPTSCALE_MODE_QUERY_PARAMETER_NAME]: mode,
29+
...restQueryParams
30+
} = currentQueryParams as {
31+
[USER_EMAIL_QUERY_PARAMETER_NAME]: string;
32+
[OPTSCALE_MODE_QUERY_PARAMETER_NAME]: string;
2733
};
2834

29-
const getEmailParameter = () => `${USER_EMAIL_QUERY_PARAMETER_NAME}=${email}`;
35+
const url = new URL(LOGIN, window.location.origin);
36+
37+
if (currentPathName !== INITIALIZE) {
38+
url.searchParams.append(
39+
NEXT_QUERY_PARAMETER_NAME,
40+
`${currentPathName}${isEmptyObject(restQueryParams) ? "" : `?${formQueryString(restQueryParams)}`}`
41+
);
42+
}
3043

31-
const parametersString = [getNextParameter(), ...(email ? [getEmailParameter()] : [])].join("&");
44+
if (email) {
45+
url.searchParams.append(USER_EMAIL_QUERY_PARAMETER_NAME, email);
46+
}
3247

33-
const to = `${LOGIN}?${parametersString}`;
48+
if (mode) {
49+
url.searchParams.append(OPTSCALE_MODE_QUERY_PARAMETER_NAME, mode);
50+
}
3451

35-
return <Navigate to={to} />;
52+
return <Navigate to={`${url.pathname}${url.search}`} />;
3653
};
3754

3855
const RouteRender = ({ isTokenRequired, component, layout, context }) => {

ngui/ui/src/components/CapabilityCard/CapabilityCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const CapabilityCard = ({
7171
elevation={0}
7272
sx={{
7373
border: (theme) => `1px solid ${lighten(theme.palette.info.main, 0.8)}`,
74+
"&:hover": {
75+
backgroundColor: (theme) => theme.palette.action.hover
76+
},
7477
...sx
7578
}}
7679
>

ngui/ui/src/components/EmailVerification/EmailVerification.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { useNavigate } from "react-router-dom";
88
import Greeter from "components/Greeter";
99
import ConfirmEmailVerificationCodeContainer from "containers/ConfirmEmailVerificationCodeContainer";
1010
import { initialize } from "containers/InitializeContainer/redux";
11-
import { INITIALIZE, SHOW_POLICY_QUERY_PARAM } from "urls";
11+
import { INITIALIZE, OPTSCALE_MODE_QUERY_PARAMETER_NAME, SHOW_POLICY_QUERY_PARAM } from "urls";
1212
import { SPACING_2 } from "utils/layouts";
1313
import macaroon from "utils/macaroons";
14-
import { formQueryString } from "utils/network";
14+
import { formQueryString, getQueryParams } from "utils/network";
1515

1616
const CONFIRM_VERIFICATION_CODE = 0;
1717
const EMAIL_VERIFICATION_SUCCESS = 1;
@@ -51,10 +51,15 @@ const EmailVerification = () => {
5151
component="button"
5252
onClick={() => {
5353
const caveats = macaroon.processCaveats(macaroon.deserialize(verificationCodeToken.token).getCaveats());
54+
const { [OPTSCALE_MODE_QUERY_PARAMETER_NAME]: mode } = getQueryParams() as {
55+
[OPTSCALE_MODE_QUERY_PARAMETER_NAME]: string;
56+
};
57+
5458
dispatch(initialize({ ...verificationCodeToken, caveats }));
5559
navigate(
5660
`${INITIALIZE}?${formQueryString({
57-
[SHOW_POLICY_QUERY_PARAM]: true
61+
[SHOW_POLICY_QUERY_PARAM]: true,
62+
[OPTSCALE_MODE_QUERY_PARAMETER_NAME]: mode
5863
})}`
5964
);
6065
}}

ngui/ui/src/components/ExpensesBreakdown/Table/ExpensesBreakdownTable.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,30 @@ import IconButton from "components/IconButton";
1010
import PoolLabel from "components/PoolLabel";
1111
import Table from "components/Table";
1212
import TableLoader from "components/TableLoader";
13+
import Tooltip from "components/Tooltip";
1314
import { FORMATTED_MONEY_TYPES, EXPENSES_FILTERBY_TYPES } from "utils/constants";
15+
import { sliceByLimitWithEllipsis } from "utils/strings";
16+
17+
const MAX_NAME_LENGTH = 64;
1418

1519
const getNameCellContentGetter = (filterBy) => {
16-
const getNameLabel = ({ link, name }) =>
17-
link ? (
18-
<Link to={link} component={RouterLink}>
19-
{name}
20-
</Link>
21-
) : (
22-
name
20+
const getNameLabel = ({ link, name }) => {
21+
const isNameLong = name.length > MAX_NAME_LENGTH;
22+
23+
const nameToDisplay = isNameLong ? sliceByLimitWithEllipsis(name, MAX_NAME_LENGTH) : name;
24+
25+
return (
26+
<Tooltip title={isNameLong ? name : undefined}>
27+
{link ? (
28+
<Link to={link} component={RouterLink}>
29+
{nameToDisplay}
30+
</Link>
31+
) : (
32+
<span>{nameToDisplay}</span>
33+
)}
34+
</Tooltip>
2335
);
36+
};
2437

2538
const getDataSourceNameCellContent = (original) => <CloudLabel label={getNameLabel(original)} type={original.type} />;
2639

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,57 @@
1-
import { Box, Stack, Typography } from "@mui/material";
2-
import CircularProgress from "@mui/material/CircularProgress";
3-
import { FormattedMessage } from "react-intl";
4-
import ButtonLoader from "components/ButtonLoader";
1+
import { Box, Stack } from "@mui/material";
52
import Logo from "components/Logo";
6-
import PageTitle from "components/PageTitle";
7-
import { SPACING_4 } from "utils/layouts";
3+
import { SPACING_6 } from "utils/layouts";
4+
import Loading from "./Loading";
5+
import Retry from "./Retry";
6+
import SetupCapability from "./SetupCapability";
87

98
type GenerateLiveDemoProps = {
109
retry: () => void;
1110
isLoading?: boolean;
1211
showRetry?: boolean;
12+
organizationId?: string;
13+
onSetupCapabilityError: () => void;
14+
onSetupCapabilitySuccess: () => void;
1315
};
1416

15-
const GenerateLiveDemo = ({ retry, isLoading = false, showRetry = false }: GenerateLiveDemoProps) => (
16-
<Stack spacing={SPACING_4} alignItems="center">
17-
<Box>
18-
<Logo width={200} dataTestId="img_logo" />
19-
</Box>
20-
{isLoading ? (
21-
<>
22-
<Box pl={2} pr={2}>
23-
<PageTitle dataTestId="p_preparing_ld" align="center">
24-
<FormattedMessage id="preparingLiveDemoMessage" />
25-
</PageTitle>
26-
<Typography align="center" data-test-id="p_process_ld">
27-
<FormattedMessage
28-
id="usuallyTheProcessTakesLessThanSeconds"
29-
values={{
30-
value: 20
31-
}}
32-
/>
33-
</Typography>
34-
</Box>
35-
<Box height={60}>
36-
<CircularProgress data-test-id="svg_loading" />
37-
</Box>
38-
</>
39-
) : (
40-
<>
41-
{showRetry ? (
42-
<>
43-
<Box pl={2} pr={2}>
44-
<PageTitle dataTestId="title_failed-live-demo" align="center">
45-
<FormattedMessage id="failedLiveDemoMessage" />
46-
</PageTitle>
47-
</Box>
48-
<Box>
49-
<ButtonLoader
50-
size="large"
51-
messageId="retry"
52-
color="primary"
53-
variant="contained"
54-
onClick={retry}
55-
isLoading={isLoading}
56-
/>
57-
</Box>
58-
</>
59-
) : null}
60-
</>
61-
)}
62-
</Stack>
63-
);
17+
const GenerateLiveDemo = ({
18+
retry,
19+
isLoading = false,
20+
showRetry = false,
21+
organizationId,
22+
onSetupCapabilitySuccess,
23+
onSetupCapabilityError
24+
}: GenerateLiveDemoProps) => {
25+
const renderContent = () => {
26+
if (isLoading) {
27+
return <Loading />;
28+
}
29+
30+
if (showRetry) {
31+
return <Retry retry={retry} />;
32+
}
33+
34+
if (organizationId) {
35+
return (
36+
<SetupCapability
37+
organizationId={organizationId}
38+
onSuccess={onSetupCapabilitySuccess}
39+
onError={onSetupCapabilityError}
40+
/>
41+
);
42+
}
43+
44+
return null;
45+
};
46+
47+
return (
48+
<Stack spacing={SPACING_6} alignItems="center">
49+
<Box>
50+
<Logo width={200} dataTestId="img_logo" />
51+
</Box>
52+
{renderContent()}
53+
</Stack>
54+
);
55+
};
6456

6557
export default GenerateLiveDemo;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Box, Typography } from "@mui/material";
2+
import CircularProgress from "@mui/material/CircularProgress";
3+
import { FormattedMessage } from "react-intl";
4+
import PageTitle from "components/PageTitle";
5+
6+
const SECONDS_TO_LOAD = 20;
7+
8+
const Loading = () => {
9+
return (
10+
<>
11+
<Box px={2}>
12+
<PageTitle dataTestId="p_preparing_ld" align="center">
13+
<FormattedMessage id="preparingLiveDemoMessage" />
14+
</PageTitle>
15+
<Typography align="center" data-test-id="p_process_ld">
16+
<FormattedMessage
17+
id="usuallyTheProcessTakesLessThanSeconds"
18+
values={{
19+
value: SECONDS_TO_LOAD
20+
}}
21+
/>
22+
</Typography>
23+
</Box>
24+
<Box height={60}>
25+
<CircularProgress data-test-id="svg_loading" />
26+
</Box>
27+
</>
28+
);
29+
};
30+
31+
export default Loading;

0 commit comments

Comments
 (0)