Skip to content

Commit

Permalink
Merge branch 'master' into newSentry
Browse files Browse the repository at this point in the history
  • Loading branch information
adonispuente authored Sep 12, 2024
2 parents 38c88f0 + 3e365d1 commit 4168a53
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 15 deletions.
1 change: 1 addition & 0 deletions config/webpack.plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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
5 changes: 5 additions & 0 deletions frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ objects:
manifestLocation: "/apps/chrome/js/fed-mods.json"
config:
ssoUrl: ${SSO_URL}
modules:
- id: 'satellite-token'
module: './SatelliteToken'
routes:
- pathname: /insights/satellite
frontend:
paths:
- /
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 />} />}
<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;
Loading

0 comments on commit 4168a53

Please sign in to comment.