Skip to content

Commit d1158df

Browse files
committed
EDM-1982: Add option to copy login command
1 parent 44793e3 commit d1158df

File tree

27 files changed

+715
-93
lines changed

27 files changed

+715
-93
lines changed

apps/ocp-plugin/src/components/common/WithPageLayout.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
import * as React from 'react';
22
import OrganizationGuard from '@flightctl/ui-components/src/components/common/OrganizationGuard';
3+
import PageNavigation from '@flightctl/ui-components/src/components/common/PageNavigation';
4+
import { AuthType } from '@flightctl/ui-components/src/types/extraTypes';
35

46
// Restore WithPageLayoutContent when organizations are enabled for OCP plugin
57
// The context is still needed since "useOrganizationGuardContext" is used in common components
6-
/*
78
const WithPageLayoutContent = ({ children }: React.PropsWithChildren) => {
8-
const { isOrganizationSelectionRequired } = useOrganizationGuardContext();
9-
10-
return isOrganizationSelectionRequired ? (
11-
<OrganizationSelector isFirstLogin />
12-
) : (
9+
return (
1310
<>
14-
<PageNavigation />
11+
<PageNavigation authType={AuthType.K8S} />
1512
{children}
1613
</>
1714
);
1815
};
19-
*/
2016

2117
const WithPageLayout = ({ children }: React.PropsWithChildren) => {
2218
return (
2319
<OrganizationGuard>
24-
<>{children}</>
20+
<WithPageLayoutContent>{children}</WithPageLayoutContent>
2521
</OrganizationGuard>
2622
);
2723
};

apps/standalone/scripts/setup_env.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ ENABLE_ORGANIZATIONS=${ENABLE_ORGANIZATIONS:-false}
2929
# Set core environment variables for kind development
3030
export FLIGHTCTL_SERVER_INSECURE_SKIP_VERIFY='true'
3131
export FLIGHTCTL_SERVER="https://$EXTERNAL_IP:3443"
32+
export FLIGHTCTL_EXTERNAL_URL="https://api.$EXTERNAL_IP.nip.io:3443"
33+
3234

3335
# CLI Artifacts - conditionally set or unset
3436
if [ "$ENABLE_CLI_ARTIFACTS" = "true" ]; then
@@ -54,6 +56,7 @@ fi
5456
echo "Environment variables set:" >&2
5557
echo " FLIGHTCTL_SERVER_INSECURE_SKIP_VERIFY=$FLIGHTCTL_SERVER_INSECURE_SKIP_VERIFY" >&2
5658
echo " FLIGHTCTL_SERVER=$FLIGHTCTL_SERVER" >&2
59+
echo " FLIGHTCTL_EXTERNAL_URL=$FLIGHTCTL_EXTERNAL_URL" >&2
5760
echo " FLIGHTCTL_CLI_ARTIFACTS_SERVER=${FLIGHTCTL_CLI_ARTIFACTS_SERVER:-'(disabled)'}" >&2
5861
echo " FLIGHTCTL_ALERTMANAGER_PROXY=${FLIGHTCTL_ALERTMANAGER_PROXY:-'(disabled)'}" >&2
5962
echo " ORGANIZATIONS_ENABLED=$ORGANIZATIONS_ENABLED" >&2

apps/standalone/src/app/components/AppLayout/AppLayout.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ import { useTranslation } from '@flightctl/ui-components/src/hooks/useTranslatio
2323

2424
import logo from '@fctl-assets/bgimages/flight-control-logo.svg';
2525
import rhemLogo from '@fctl-assets/bgimages/RHEM-logo.svg';
26+
import { AuthContext } from '../../context/AuthContext';
2627
import AppNavigation from './AppNavigation';
2728
import AppToolbar from './AppToolbar';
2829

30+
const pageId = 'primary-app-container';
31+
2932
const AppLayoutContent = () => {
3033
const { t } = useTranslation();
3134
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
3235

3336
const { isOrganizationSelectionRequired } = useOrganizationGuardContext();
37+
const { authType } = React.useContext(AuthContext);
3438

3539
const onSidebarToggle = () => {
3640
setIsSidebarOpen((prevIsOpen) => !prevIsOpen);
@@ -78,8 +82,6 @@ const AppLayoutContent = () => {
7882
</PageSidebar>
7983
);
8084

81-
const pageId = 'primary-app-container';
82-
8385
const PageSkipToContent = (
8486
<SkipToContent
8587
onClick={(event) => {
@@ -98,7 +100,7 @@ const AppLayoutContent = () => {
98100
<OrganizationSelector isFirstLogin />
99101
) : (
100102
<>
101-
<PageNavigation />
103+
<PageNavigation authType={authType} />
102104
<Outlet />
103105
</>
104106
)}
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
.fctl-app_toolbar,
2-
.fctl-subnav_toolbar {
1+
.fctl-app_toolbar {
32
justify-content: flex-end;
43
}
5-
6-
/* Extra navigation bar for global actions (organization switcher, copy login command, etc.) */
7-
/* We make it as tall as the navigation menu items on the left */
8-
#global-actions-masthead {
9-
padding: 0;
10-
--pf-v5-c-masthead--m-display-inline__content--MinHeight: 3.5rem;
11-
}
12-
13-
#global-actions-masthead .fctl-subnav_toolbar {
14-
--pf-v5-c-toolbar--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-300);
15-
}

apps/standalone/src/app/components/AppLayout/AppToolbar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useTranslation } from '@flightctl/ui-components/src/hooks/useTranslatio
2020
import { ROUTE, useNavigate } from '@flightctl/ui-components/src/hooks/useNavigate';
2121

2222
import { getErrorMessage } from '@flightctl/ui-components/src/utils/error';
23+
import { AuthType } from '@flightctl/ui-components/src/types/extraTypes';
2324
import { AuthContext } from '../../context/AuthContext';
2425
import { logout } from '../../utils/apiCalls';
2526

@@ -69,14 +70,14 @@ const AppToolbar = () => {
6970
const [preferencesModalOpen, setPreferencesModalOpen] = React.useState(false);
7071
const [helpDropdownOpen, setHelpDropdownOpen] = React.useState<boolean>(false);
7172

72-
const { username, authEnabled } = React.useContext(AuthContext);
73+
const { username, authType } = React.useContext(AuthContext);
7374
const [logoutLoading, setLogoutLoading] = React.useState(false);
7475
const [logoutErr, setLogoutErr] = React.useState<string>();
7576
const onUserPreferences = () => setPreferencesModalOpen(true);
7677
const navigate = useNavigate();
7778

7879
let userDropdown = <UserDropdown onUserPreferences={onUserPreferences} />;
79-
if (authEnabled && username) {
80+
if (authType !== AuthType.DISABLED && username) {
8081
userDropdown = (
8182
<UserDropdown username={username} onUserPreferences={onUserPreferences}>
8283
<DropdownItem

apps/standalone/src/app/context/AuthContext.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { loginAPI, redirectToLogin } from '../utils/apiCalls';
33
import { ORGANIZATION_STORAGE_KEY } from '@flightctl/ui-components/src/utils/organizationStorage';
4+
import { AuthType } from '@flightctl/ui-components/src/types/extraTypes';
5+
import { OAUTH_REDIRECT_AFTER_LOGIN_KEY } from '@flightctl/ui-components/src/constants';
46

57
const AUTH_DISABLED_STATUS_CODE = 418;
68
const EXPIRATION = 'expiration';
@@ -11,38 +13,56 @@ const maxTimeout = 2 ** 31 - 1;
1113

1214
const nowInSeconds = () => Math.round(Date.now() / 1000);
1315

16+
const secondarySessionRedirectPages = ['copy-login-command'];
17+
1418
type AuthContextProps = {
19+
authType: AuthType;
1520
username: string;
1621
loading: boolean;
17-
authEnabled: boolean;
1822
error: string | undefined;
1923
};
2024

2125
export const AuthContext = React.createContext<AuthContextProps>({
26+
authType: AuthType.DISABLED,
2227
username: '',
23-
authEnabled: true,
2428
loading: false,
2529
error: undefined,
2630
});
2731

2832
export const useAuthContext = () => {
2933
const [username, setUsername] = React.useState('');
3034
const [loading, setLoading] = React.useState(true);
31-
const [authEnabled, setAuthEnabled] = React.useState(true);
35+
const [authType, setAuthType] = React.useState<AuthType>(AuthType.DISABLED);
3236
const [error, setError] = React.useState<string>();
3337
const refreshRef = React.useRef<NodeJS.Timeout>();
3438

3539
React.useEffect(() => {
3640
const getUserInfo = async () => {
3741
let callbackErr: string | null = null;
3842
if (window.location.pathname === '/callback') {
39-
localStorage.removeItem(EXPIRATION);
40-
localStorage.removeItem(ORGANIZATION_STORAGE_KEY);
4143
const searchParams = new URLSearchParams(window.location.search);
4244
const code = searchParams.get('code');
4345
callbackErr = searchParams.get('error');
46+
4447
if (code) {
45-
const resp = await fetch(loginAPI, {
48+
// Some pages require the user to re-authenticate for security reasons.
49+
// The token generated for these "secondary sessions" is independent of the primary session token.
50+
const redirectAfterLogin = localStorage.getItem(OAUTH_REDIRECT_AFTER_LOGIN_KEY);
51+
const isPrimarySession = !secondarySessionRedirectPages.includes(redirectAfterLogin || '');
52+
53+
let loginEndpoint: string;
54+
if (isPrimarySession) {
55+
loginEndpoint = loginAPI;
56+
localStorage.removeItem(ORGANIZATION_STORAGE_KEY);
57+
localStorage.removeItem(EXPIRATION);
58+
} else {
59+
// Do not clear the localStorage items, otherwise they would be unset for the primary session too
60+
// We will force a new authentication flow that will allow us to retrieve the token from a newly generated sessionId
61+
loginEndpoint = `${loginAPI}/create-session-token`;
62+
}
63+
64+
// In both cases, we trigger a new login flow
65+
const resp = await fetch(loginEndpoint, {
4666
headers: {
4767
'Content-Type': 'application/json',
4868
},
@@ -52,11 +72,16 @@ export const useAuthContext = () => {
5272
code: code,
5373
}),
5474
});
55-
const expiration = (await resp.json()) as { expiresIn: number };
56-
if (expiration.expiresIn) {
75+
76+
if (isPrimarySession) {
77+
const newLoginResponse = (await resp.json()) as { expiresIn: number };
5778
const now = nowInSeconds();
58-
localStorage.setItem(EXPIRATION, `${now + expiration.expiresIn}`);
79+
localStorage.setItem(EXPIRATION, `${now + newLoginResponse.expiresIn}`);
5980
lastRefresh = now;
81+
} else {
82+
const newLoginResponse = (await resp.json()) as { sessionId: string };
83+
localStorage.removeItem(OAUTH_REDIRECT_AFTER_LOGIN_KEY);
84+
window.location.href = `/${redirectAfterLogin}?sessionId=${newLoginResponse.sessionId || ''}`;
6085
}
6186
} else if (callbackErr) {
6287
setError(callbackErr);
@@ -69,7 +94,7 @@ export const useAuthContext = () => {
6994
credentials: 'include',
7095
});
7196
if (resp.status === AUTH_DISABLED_STATUS_CODE) {
72-
setAuthEnabled(false);
97+
setAuthType(AuthType.DISABLED);
7398
setLoading(false);
7499
return;
75100
}
@@ -81,8 +106,9 @@ export const useAuthContext = () => {
81106
setError('Failed to get user info');
82107
return;
83108
}
84-
const info = (await resp.json()) as { username: string };
109+
const info = (await resp.json()) as { username: string; authType: AuthType };
85110
setUsername(info.username);
111+
setAuthType(info.authType);
86112
setLoading(false);
87113
} catch (err) {
88114
// eslint-disable-next-line
@@ -98,7 +124,7 @@ export const useAuthContext = () => {
98124
React.useEffect(() => {
99125
if (!loading) {
100126
const scheduleRefresh = () => {
101-
if (!authEnabled) {
127+
if (authType === AuthType.DISABLED) {
102128
return;
103129
}
104130
const expiresAt = parseInt(localStorage.getItem(EXPIRATION) || '0', 10);
@@ -138,7 +164,7 @@ export const useAuthContext = () => {
138164
scheduleRefresh();
139165
}
140166
return () => clearTimeout(refreshRef.current);
141-
}, [loading, authEnabled]);
167+
}, [loading, authType]);
142168

143-
return { username, loading, authEnabled, error };
169+
return { username, loading, authType, error };
144170
};

apps/standalone/src/app/routes.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ const PendingEnrollmentRequestsBadge = React.lazy(
6868
const CommandLineToolsPage = React.lazy(
6969
() => import('@flightctl/ui-components/src/components/Masthead/CommandLineToolsPage'),
7070
);
71+
const CopyLoginCommandPage = React.lazy(
72+
() => import('@flightctl/ui-components/src/components/Masthead/CopyLoginCommandPage'),
73+
);
7174

7275
export type ExtendedRouteObject = RouteObject & {
7376
title?: string;
@@ -310,6 +313,7 @@ const AppRouter = () => {
310313
const { t } = useTranslation();
311314

312315
const { loading, error } = React.useContext(AuthContext);
316+
313317
if (error) {
314318
return (
315319
<Bullseye>
@@ -347,6 +351,15 @@ const AppRouter = () => {
347351
errorElement: <ErrorPage />,
348352
children: getAppRoutes(t),
349353
},
354+
// Route is only exposed for the standalone app, and it doesn't inherit the app layout
355+
{
356+
path: '/copy-login-command',
357+
element: (
358+
<TitledRoute title={t('Copy login command')}>
359+
<CopyLoginCommandPage />
360+
</TitledRoute>
361+
),
362+
},
350363
]);
351364

352365
return <RouterProvider router={router} />;

libs/i18n/locales/en/translation.json

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"Resource sync": "Resource sync",
3535
"Failed to login": "Failed to login",
3636
"Try again": "Try again",
37+
"Copy login command": "Copy login command",
3738
"No devices": "No devices",
3839
"Restricted Access": "Restricted Access",
3940
"You don't have access to this section.": "You don't have access to this section.",
@@ -62,7 +63,13 @@
6263
"Reload": "Reload",
6364
"Cancel": "Cancel",
6465
"Download": "Download",
66+
"Copied!": "Copied!",
6567
"Copy text": "Copy text",
68+
"{{ brandName }} CLI authentication": "{{ brandName }} CLI authentication",
69+
"Copy and run this command in your terminal to authenticate with {{ brandName }}:": "Copy and run this command in your terminal to authenticate with {{ brandName }}:",
70+
"Loading...": "Loading...",
71+
"Next steps": "Next steps",
72+
"After running this command, you'll be authenticated and can use the {{ brandName }} CLI to manage your edge devices from your terminal.": "After running this command, you'll be authenticated and can use the {{ brandName }} CLI to manage your edge devices from your terminal.",
6673
"New label": "New label",
6774
"Add label": "Add label",
6875
"Unexpected error occurred": "Unexpected error occurred",
@@ -79,6 +86,8 @@
7986
"Continue": "Continue",
8087
"Select Organization": "Select Organization",
8188
"Change Organization": "Change Organization",
89+
"You will be directed to login in order to generate your login token command": "You will be directed to login in order to generate your login token command",
90+
"Get login command": "Get login command",
8291
"Technology preview features provide early access to upcoming product innovations, enabling you to test functionality and provide feedback during the development process.": "Technology preview features provide early access to upcoming product innovations, enabling you to test functionality and provide feedback during the development process.",
8392
"Technology preview description": "Technology preview description",
8493
"Technology preview": "Technology preview",
@@ -577,13 +586,24 @@
577586
"pending device": "pending device",
578587
"resource sync": "resource sync",
579588
"You are about to resume device <1>{deviceName}</1>": "You are about to resume device <1>{deviceName}</1>",
580-
"No {{ productName }} command line tools were found for this deployment at this time.": "No {{ productName }} command line tools were found for this deployment at this time.",
581-
"Could not list the {{ productName }} command line tools": "Could not list the {{ productName }} command line tools",
589+
"No {{ brandName }} command line tools were found for this deployment at this time.": "No {{ brandName }} command line tools were found for this deployment at this time.",
590+
"Could not list the {{ brandName }} command line tools": "Could not list the {{ brandName }} command line tools",
582591
"Download flightctl CLI for {{ os }} for {{ arch }}": "Download flightctl CLI for {{ os }} for {{ arch }}",
583-
"Red Hat Edge Manager": "Red Hat Edge Manager",
584-
"Flight Control": "Flight Control",
585-
"With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.": "With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.",
586-
"Command line tools are not available for download in this {{ productName }} installation.": "Command line tools are not available for download in this {{ productName }} installation.",
592+
"With the {{ brandName }} command line interface, you can manage your fleets, devices and repositories from a terminal.": "With the {{ brandName }} command line interface, you can manage your fleets, devices and repositories from a terminal.",
593+
"Command line tools are not available for download in this {{ brandName }} installation.": "Command line tools are not available for download in this {{ brandName }} installation.",
594+
"Login successful!": "Login successful!",
595+
"Copy and run this command in your terminal to authenticate to {{ brandName }}:": "Copy and run this command in your terminal to authenticate to {{ brandName }}:",
596+
"Show more": "Show more",
597+
"Show Less": "Show Less",
598+
"Show More": "Show More",
599+
"Failed to obtain session token": "Failed to obtain session token",
600+
"This URL can only be used with a valid session ID": "This URL can only be used with a valid session ID",
601+
"The login command for this session is no longer available": "The login command for this session is no longer available",
602+
"Error getting session token": "Error getting session token",
603+
"This session's login token was already retrieved once, or you used the wrong URL.": "This session's login token was already retrieved once, or you used the wrong URL.",
604+
"Please return to {{ brandName }} and request a new login command.": "Please return to {{ brandName }} and request a new login command.",
605+
"Back to {{ brandName }}": "Back to {{ brandName }}",
606+
"CLI authentication portal": "CLI authentication portal",
587607
"System default": "System default",
588608
"Light": "Light",
589609
"Dark": "Dark",
@@ -790,8 +810,8 @@
790810
"Overall status of application workloads.": "Overall status of application workloads.",
791811
"Overall status of device hardware and operating system.": "Overall status of device hardware and operating system.",
792812
"Current system configuration vs. latest system configuration.": "Current system configuration vs. latest system configuration.",
793-
"{{brand}} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.": "{{brand}} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.",
794-
"{{brand}} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.": "{{brand}} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.",
813+
"{{ brandName }} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.": "{{ brandName }} is waiting for the device to connect and report its status. It will report a ʼPending syncʼ status until it is able to reconnect. If it has configuration conflicts, it will report a ʼSuspendedʼ status and require manual action to resume.",
814+
"{{ brandName }} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.": "{{ brandName }} is waiting for devices to connect and report their status. Devices will report a ʼPending syncʼ status until they are able to connect. Devices with configuration conflicts will report a ʼSuspendedʼ status and require manual action to resume.",
795815
"System recovery complete": "System recovery complete",
796816
"This device is suspended because its local configuration is newer than the server's record. It will not receive updates until it is resumed.": "This device is suspended because its local configuration is newer than the server's record. It will not receive updates until it is resumed.",
797817
"<0>{suspendedCountStr}</0> <2>devices in this fleet</2> are suspended because their local configuration is newer than the server&apos;s record. These devices will not receive updates until they are resumed._one": "<0>{suspendedCountStr}</0> <2>device in this fleet</2> is suspended because its local configuration is newer than the server&apos;s record. This device will not receive updates until it is resumed.",

0 commit comments

Comments
 (0)