Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ip address whitelist UI to manage satellites page for itless #2836

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/webpack.plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const plugins = (dev = false, beta = false, restricted = false) => {
'./DownloadButton': resolve(__dirname, '../src/pdf/DownloadButton.tsx'),
'./LandingNavFavorites': resolve(__dirname, '../src/components/FavoriteServices/LandingNavFavorites.tsx'),
'./DashboardFavorites': resolve(__dirname, '../src/components/FavoriteServices/DashboardFavorites.tsx'),
'./SatelliteToken': resolve(__dirname, '../src/layouts/SatelliteToken.tsx'),
},
shared: [
{ react: { singleton: true, eager: true, requiredVersion: deps.react } },
Expand Down
10 changes: 5 additions & 5 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Fragment, Suspense, useContext, useState } from 'react';
import ReactDOM from 'react-dom';
import { useFlag } from '@unleash/proxy-client-react';
import Tools from './Tools';
import UnAuthtedHeader from './UnAuthtedHeader';
import { MastheadBrand, MastheadContent, MastheadMain } from '@patternfly/react-core/dist/dynamic/components/Masthead';
Expand All @@ -15,7 +16,6 @@ import { DeepRequired } from 'utility-types';

import './Header.scss';
import { activationRequestURLs } from '../../utils/consts';
import { ITLess } from '../../utils/common';
import SearchInput from '../Search/SearchInput';
import AllServicesDropdown from '../AllServicesDropdown/AllServicesDropdown';
import Breadcrumbs, { Breadcrumbsprops } from '../Breadcrumbs/Breadcrumbs';
Expand All @@ -40,14 +40,14 @@ export const Header = ({ breadcrumbsProps }: { breadcrumbsProps?: Breadcrumbspro
const { user } = useContext(ChromeAuthContext) as DeepRequired<ChromeAuthContextValue>;
const search = new URLSearchParams(window.location.search).keys().next().value;
const isActivationPath = activationRequestURLs.includes(search);
const isITLessEnv = ITLess();
const { pathname } = useLocation();
const noBreadcrumb = !['/', '/allservices', '/favoritedservices'].includes(pathname);
const { md, lg } = useWindowWidth();
const [searchOpen, setSearchOpen] = useState(false);
const hideAllServices = (isOpen: boolean) => {
setSearchOpen(isOpen);
};
const isITLess = useFlag('platform.chrome.itless');

return (
<Fragment>
Expand All @@ -64,18 +64,18 @@ export const Header = ({ breadcrumbsProps }: { breadcrumbsProps?: Breadcrumbspro
</Toolbar>
</MastheadMain>
<MastheadContent className="pf-v5-u-mx-md pf-v5-u-mx-0-on-2xl">
{user?.identity?.org_id && !isITLessEnv && ReactDOM.createPortal(<FeedbackRoute />, document.body)}
{user?.identity?.org_id && !isITLess && ReactDOM.createPortal(<FeedbackRoute />, document.body)}
{user && isActivationPath && <Activation user={user} request={search} />}
<Toolbar isFullHeight>
<ToolbarContent>
<ToolbarGroup variant="filter-group">
{user && (
<ToolbarItem>
{!(!md && searchOpen) && <AllServicesDropdown />}
{isITLessEnv && user?.identity?.user?.is_org_admin && <SatelliteLink />}
{isITLess && user?.identity?.user?.is_org_admin && <SatelliteLink />}
</ToolbarItem>
)}
{user && !isITLessEnv && (
{user && !isITLess && (
<ToolbarItem className="pf-v5-m-hidden pf-v5-m-visible-on-xl">
<ContextSwitcher user={user} className="data-hj-suppress sentry-mask" />
</ToolbarItem>
Expand Down
2 changes: 0 additions & 2 deletions src/components/RootApp/ScalprumRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import chromeHistory from '../../utils/chromeHistory';
import DefaultLayout from '../../layouts/DefaultLayout';
import AllServices from '../../layouts/AllServices';
import FavoritedServices from '../../layouts/FavoritedServices';
import SatelliteToken from '../../layouts/SatelliteToken';
import historyListener from '../../utils/historyListener';
import SegmentContext from '../../analytics/SegmentContext';
import LoadingFallback from '../../utils/loading-fallback';
Expand Down Expand Up @@ -259,7 +258,6 @@ const ScalprumRoot = memo(
}
/>
)}
{ITLess() && <Route path="/insights/satellite" element={<SatelliteToken />} />}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the react router route in ScalprumRoot in favor of the exposed module approach.

<Route path="/security" element={<DefaultLayout {...props} />} />
<Route path="*" element={<DefaultLayout Sidebar={Navigation} {...props} />} />
</Routes>
Expand Down
259 changes: 259 additions & 0 deletions src/components/Satellite/IPWhitelistTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import React, { useEffect, useState } from 'react';
import { debounce } from 'lodash';
import {
ActionGroup,
Bullseye,
Button,
EmptyState,
EmptyStateBody,
EmptyStateHeader,
EmptyStateVariant,
Form,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
Modal,
ModalVariant,
Text,
TextContent,
TextInput,
ValidatedOptions,
} from '@patternfly/react-core';
import { InnerScrollContainer, OuterScrollContainer, Table, TableText, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import axios from 'axios';
import SkeletonTable from '@redhat-cloud-services/frontend-components/SkeletonTable';

type IPBlock = {
ip_block: string;
org_id: string;
created_at: string;
};

const IPWhitelistTable: React.FC = () => {
const [allAddresses, setAllAddresses] = useState<IPBlock[]>([]);
const [loaded, setLoaded] = useState(false);
const [actionPending, setActionPending] = useState(false);
const [inputAddresses, setInputAddresses] = useState('');
const [inputAddressesValidated, setInputAddressesesValidated] = useState(false);
const [removeAddresses, setRemoveAddresses] = useState('');
const [isIPModalOpen, setIsIPModalOpen] = useState(false);
const [isIPRemoveModalOpen, setIsIPRemoveModalOpen] = useState(false);

const getIPAddresses = () => {
return axios.get('/api/mbop/v1/allowlist');
};

const removeIPAddresses = (ipBlock: string) => {
return axios.delete(`/api/mbop/v1/allowlist?block=${ipBlock}`);
};

const addIPAddresses = (ipBlock: string) => {
return axios.post('/api/mbop/v1/allowlist', { ip_block: ipBlock });
};

useEffect(() => {
if (!loaded && !actionPending) {
getIPAddresses()
.then((res) => {
setAllAddresses(res.data);
setLoaded(true);
})
.catch((err) => console.error(err));
}
}, [loaded, actionPending]);

const onChangedAddresses = (value: string) => {
setInputAddresses(value);
setInputAddressesesValidated(validateIPAddress(value));
};

const onSubmitAddresses = () => {
setActionPending(true);
addIPAddresses(inputAddresses)
.then(() => {
setInputAddresses('');
setIsIPModalOpen(false);
setLoaded(false);
return getIPAddresses();
})
.then((res) => {
setAllAddresses(res.data);
setLoaded(true);
})
.catch((err) => console.error(err))
.finally(() => setActionPending(false));
};

const onRemoveAddresses = () => {
setActionPending(true);
removeIPAddresses(removeAddresses)
.then(() => {
setRemoveAddresses('');
setIsIPRemoveModalOpen(false);
setLoaded(false);
return getIPAddresses();
})
.then((res) => {
setAllAddresses(res.data);
setLoaded(true);
})
.catch((err) => console.error(err))
.finally(() => setActionPending(false));
};

const onChangedAddressesDebounced = debounce(onChangedAddresses, 500);

const validateIPAddress = (address: string) => {
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([1-9]|[12][0-9]|3[0-2]))?$/.test(
address
);
};

const validationError = inputAddresses.length > 0 && !inputAddressesValidated;

const addIPModal = (
<Modal
isOpen={isIPModalOpen}
onClose={() => {
setInputAddresses('');
setIsIPModalOpen(false);
}}
title={'Add IP Addresses to Allow List'}
variant={ModalVariant.medium}
>
<Form onSubmit={(event: React.FormEvent<HTMLFormElement>) => event.preventDefault()}>
<FormGroup>
<TextContent>
<Text>Before connecting to your satellite servers, Red Hat needs to add your IP address or range of IP addresses to an allow-list.</Text>
</TextContent>
<TextInput
validated={validationError ? ValidatedOptions.error : ValidatedOptions.default}
placeholder="127.0.0.1/32"
onChange={(_event, value) => onChangedAddressesDebounced(value)}
></TextInput>
{validationError && (
<FormHelperText>
<HelperText>
<HelperTextItem icon={<ExclamationCircleIcon />} variant={ValidatedOptions.error}>
Enter a valid IP address or CIDR notation IP range
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
<ActionGroup>
<Button isDisabled={inputAddresses.length <= 0 || validationError || actionPending} onClick={onSubmitAddresses}>
Submit
</Button>
</ActionGroup>
</Form>
</Modal>
);

const removeIPModal = (
<Modal
isOpen={isIPRemoveModalOpen}
onClose={() => {
setRemoveAddresses('');
setIsIPRemoveModalOpen(false);
}}
title={'Remove IP Addresses from Allow List'}
variant={ModalVariant.medium}
>
<Form onSubmit={(event: React.FormEvent<HTMLFormElement>) => event.preventDefault()}>
<FormGroup>
<TextContent>
<Text>The following IP addresses will be removed from the allow list</Text>
</TextContent>
<TextInput isDisabled value={removeAddresses}></TextInput>
</FormGroup>
<ActionGroup>
<Button onClick={onRemoveAddresses} isDisabled={actionPending} variant="danger">
Remove
</Button>
</ActionGroup>
</Form>
</Modal>
);

const columnNames = {
ip_block: 'IP Block',
org_id: 'Org ID',
created_at: 'Created At',
remove: '',
};

const skeletonTable = <SkeletonTable variant={TableVariant.compact} rows={9} columns={Object.values(columnNames)} />;

const emptyTable = (
<Tr style={{ border: 'none' }}>
<Td colSpan={8}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader titleText="No IP Addresses Allowed" headingLevel="h2" />
<EmptyStateBody>
Before connecting to your satellite servers, Red Hat needs to add your IP address or range of IP addresses to an allow-list.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);

const ipTable = (
<OuterScrollContainer style={{ maxHeight: '25rem' }}>
<InnerScrollContainer>
<Table aria-label="IP Address Allow List" variant={TableVariant.compact} isStickyHeader>
<Thead>
<Tr>
<Th>{columnNames.ip_block}</Th>
<Th>{columnNames.org_id}</Th>
<Th>{columnNames.created_at}</Th>
<Th>{columnNames.remove}</Th>
</Tr>
</Thead>
<Tbody>
{allAddresses.length <= 0 && emptyTable}
{allAddresses.map((ipBlock) => (
<Tr key={ipBlock.ip_block}>
<Td dataLabel={columnNames.ip_block}>{ipBlock.ip_block}</Td>
<Td dataLabel={columnNames.org_id}>{ipBlock.org_id}</Td>
<Td dataLabel={columnNames.created_at}>{ipBlock.created_at}</Td>
<Td dataLabel={columnNames.remove} modifier="fitContent">
<TableText>
<Button
variant="secondary"
onClick={() => {
setRemoveAddresses(ipBlock.ip_block);
setIsIPRemoveModalOpen(true);
}}
>
Remove
</Button>
</TableText>
</Td>
</Tr>
))}
</Tbody>
</Table>
</InnerScrollContainer>
</OuterScrollContainer>
);

return (
<>
{addIPModal}
{removeIPModal}
<>
{loaded ? ipTable : skeletonTable}
<div>
<Button onClick={() => setIsIPModalOpen(true)}>Add IP Addresses</Button>
</div>
</>
</>
);
};

export default IPWhitelistTable;
29 changes: 21 additions & 8 deletions src/layouts/SatelliteToken.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import axios from 'axios';
import { Header } from '../components/Header/Header';
import { useFlag } from '@unleash/proxy-client-react';
import { Button } from '@patternfly/react-core/dist/dynamic/components/Button';
import { Card, CardBody, CardTitle } from '@patternfly/react-core/dist/dynamic/components/Card';
import { ClipboardCopy } from '@patternfly/react-core/dist/dynamic/components/ClipboardCopy';
import { List, ListComponent, ListItem, OrderType } from '@patternfly/react-core/dist/dynamic/components/List';
import { Masthead } from '@patternfly/react-core/dist/dynamic/components/Masthead';
import { Page, PageSection } from '@patternfly/react-core/dist/dynamic/components/Page';
import SatelliteTable from '../components/Satellite/SatelliteTable';
import IPWhitelistTable from '../components/Satellite/IPWhitelistTable';
import { getEnv } from '../utils/common';
import ChromeAuthContext from '../auth/ChromeAuthContext';
import NotFoundRoute from '../components/NotFoundRoute';

const SatelliteToken: React.FC = () => {
const [token, setToken] = useState('');
const [error, setError] = useState(null);
const { user } = useContext(ChromeAuthContext);
const isITLess = useFlag('platform.chrome.itless');

if (!isITLess) {
return <NotFoundRoute />;
}

const generateToken = () => {
axios
Expand All @@ -38,11 +46,6 @@ const SatelliteToken: React.FC = () => {
<Page
className="chr-c-all-services"
onPageResize={null} // required to disable PF resize observer that causes re-rendring issue
header={
<Masthead className="chr-c-masthead">
<Header />
Copy link
Contributor Author

@florkbr florkbr Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hyperkid123 since we are now exposing this as a module from chrome at /insights/satellite - this is causing the insights masthead and nav to render around the module (hence removing the masthead here otherwise we see duplicates).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this page require some unique layout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - the content is just a bit odd/out of the ordinary as it is 3 utilities.

</Masthead>
}
>
<PageSection padding={{ default: 'noPadding', md: 'padding', lg: 'padding' }}>
<Card>
Expand Down Expand Up @@ -87,6 +90,16 @@ const SatelliteToken: React.FC = () => {
</CardBody>
</Card>
</PageSection>
{user.identity.user?.is_org_admin ? (
<PageSection>
<Card>
<CardTitle>IP Address Allow List</CardTitle>
<CardBody>
<IPWhitelistTable />
</CardBody>
</Card>
</PageSection>
) : null}
</Page>
</div>
);
Expand Down
Loading