1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00

[redhat-3.16] chore: migrate SidebarState and AlertState from Recoil to React Context (#4481)

* chore: migrate SidebarState and AlertState from Recoil to React Context

Migrates UI state management from Recoil atoms to a centralized
UIContext using pure React Context API. This is part of the broader
effort to simplify state management and reduce dependencies.

Changes:
- Create UIContext with sidebar and alert state management
- Migrate SidebarState: isSidebarOpen with localStorage persistence
- Migrate AlertState: alerts array with add/remove/clear operations
- Move AlertVariant enum and AlertDetails interface to UIContext
- Remove UseAlerts hook (now redundant - consumers use useUI directly)
- Update Alerts component to use removeAlert from context
- Update QuayHeader and QuaySidebar to use useUI hook
- Update 128 files to import types/hooks from UIContext
- Delete AlertState.ts, SidebarState.ts, and UseAlerts.ts

Benefits:
- Zero runtime logic changes for consumers
- Centralized UI state in single context
- Reduced Recoil surface area (2 fewer atoms)
- Simpler architecture (removed unnecessary hook wrapper)
- Future-proof for additional UI state (theme, plugin mode)
- Pure React with no external dependencies for UI state

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Brady Pratt <bpratt@redhat.com>

* fix(ui): add toast alerts for create organization, repository, and application token operations

Adds success and failure toast alerts to create operations that previously
only showed inline error messages. This provides consistent feedback across
all CRUD operations and ensures users receive confirmation after modals close.

Also fixes browser compatibility issue in UIContext by replacing
crypto.randomUUID() with Math.random().toString(36) for alert key generation,
maintaining compatibility with the original UseAlerts implementation.

Changes:
- Add toast alerts to CreateOrganizationModal for create org operations
- Add toast alerts to CreateRepoModalTemplate for create repo operations
- Add toast alerts to CreateApplicationTokenModal for token creation
- Fix UIContext to use Math.random() instead of crypto.randomUUID()
- Maintain existing inline error displays for immediate validation feedback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ui): clear alerts on route navigation to prevent stale alerts from persisting

Adds route-level alert cleanup to StandaloneMain to clear all alerts when
navigating between routes. This prevents old alerts from accumulating and
reappearing when users return to previously visited pages.

Previously, alerts (especially failure alerts which don't auto-dismiss) would
persist in UIContext state and re-render when navigating back to a page. Now
alerts are automatically cleared whenever the route pathname changes.

Changes:
- Add useLocation hook to track route changes
- Add clearAllAlerts from UIContext
- Add useEffect to clear alerts on location.pathname change
- Ensures fresh alert state for each route

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Brady Pratt <bpratt@redhat.com>

* chore: address coderabbit feedback

- fix early return, initialize the useEffect first
- call props.toggleModal()
- fix entityType shadowing
- improve grammar in alerts for ToggleUserStatusModal

Signed-off-by: Brady Pratt <bpratt@redhat.com>

* fix(ui): wait for collaborator deletion before closing modal

The CollaboratorsDeleteModal was closing immediately after clicking delete,
causing it to unmount before the async deletion completed. This prevented
the success alert from appearing because the useEffect hook that adds the
alert never fired after component unmount.

Fixed by moving the toggleModal() call from the delete button's onClick
handler into the success and error useEffect hooks, ensuring the modal
stays open until the mutation completes and the alert is displayed.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Signed-off-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
OpenShift Cherrypick Robot
2025-11-07 00:00:08 +01:00
committed by GitHub
parent f1ff178e3b
commit 32602f5c32
79 changed files with 570 additions and 435 deletions

View File

@@ -1,19 +0,0 @@
import {ReactNode} from 'react';
import {atom} from 'recoil';
export enum AlertVariant {
Success = 'success',
Failure = 'danger',
}
export interface AlertDetails {
variant: AlertVariant;
title: string;
key?: string;
message?: string | ReactNode;
}
export const alertState = atom<AlertDetails[]>({
key: 'alertState',
default: [],
});

View File

@@ -1,8 +0,0 @@
import {atom} from 'recoil';
export const SidebarState = atom({
key: 'sidebarState',
default: {
isOpen: true,
},
});

View File

@@ -15,13 +15,12 @@ import logo from 'src/assets/quay.svg';
import rh_logo from 'src/assets/RH_QuayIO2.svg';
import {HeaderToolbar} from './HeaderToolbar';
import {Link} from 'react-router-dom';
import {SidebarState} from 'src/atoms/SidebarState';
import {useSetRecoilState} from 'recoil';
import {useUI} from 'src/contexts/UIContext';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import './QuayHeader.css';
export function QuayHeader({toggleDrawer}: {toggleDrawer: () => void}) {
const setSidebarState = useSetRecoilState(SidebarState);
const {toggleSidebar} = useUI();
const quayConfig = useQuayConfig();
let logoUrl = logo;
if (quayConfig && quayConfig.config?.BRANDING?.logo) {
@@ -33,17 +32,13 @@ export function QuayHeader({toggleDrawer}: {toggleDrawer: () => void}) {
logoUrl = rh_logo;
}
const toggleSidebarVisibility = () => {
setSidebarState((oldState) => ({isOpen: !oldState.isOpen}));
};
return (
<Masthead>
<MastheadToggle>
<Button
variant="plain"
aria-label="Global navigation"
onClick={toggleSidebarVisibility}
onClick={toggleSidebar}
>
<BarsIcon />
</Button>

View File

@@ -1,192 +1,281 @@
import {Label as ImageLabel} from 'src/resources/TagResource';
import {
Label as ImageLabel,
} from 'src/resources/TagResource';
import { Button, DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Label, Skeleton} from '@patternfly/react-core';
Button,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
Label,
Skeleton,
} from '@patternfly/react-core';
import './Labels.css';
import { useLabels } from 'src/hooks/UseTagLabels';
import { useEffect, useState } from 'react';
import {useLabels} from 'src/hooks/UseTagLabels';
import {useEffect, useState} from 'react';
import Conditional from '../empty/Conditional';
import { useAlerts } from 'src/hooks/UseAlerts';
import { AlertVariant } from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import EditableLabel from './EditableLabel';
export default function EditableLabels(props: EditableLabelsProps) {
const {
labels,
setLabels,
initialLabels,
loading,
error,
createLabels,
successCreatingLabels,
errorCreatingLabels,
errorCreatingLabelsDetails,
loadingCreateLabels,
deleteLabels,
successDeletingLabels,
errorDeletingLabels,
errorDeletingLabelsDetails,
loadingDeleteLabels,
} = useLabels(props.org, props.repo, props.digest);
const [newLabel, setNewLabel] = useState<string>('');
const [invalidNewLabel, setInvalidNewLabel] = useState<string>(null);
const { addAlert } = useAlerts();
const loadingLabelChanges: boolean = loadingCreateLabels || loadingDeleteLabels;
const readonlyLabels = labels.filter((label: ImageLabel) => label.source_type !== 'api');
const mutableLabels = labels.filter((label: ImageLabel) => label.source_type === 'api');
const {
labels,
setLabels,
initialLabels,
loading,
error,
createLabels,
successCreatingLabels,
errorCreatingLabels,
errorCreatingLabelsDetails,
loadingCreateLabels,
deleteLabels,
successDeletingLabels,
errorDeletingLabels,
errorDeletingLabelsDetails,
loadingDeleteLabels,
} = useLabels(props.org, props.repo, props.digest);
const [newLabel, setNewLabel] = useState<string>('');
const [invalidNewLabel, setInvalidNewLabel] = useState<string>(null);
const {addAlert} = useUI();
const loadingLabelChanges: boolean =
loadingCreateLabels || loadingDeleteLabels;
const readonlyLabels = labels.filter(
(label: ImageLabel) => label.source_type !== 'api',
);
const mutableLabels = labels.filter(
(label: ImageLabel) => label.source_type === 'api',
);
if (error) {
return <>Unable to get labels</>;
useEffect(() => {
if (successCreatingLabels) {
addAlert({
variant: AlertVariant.Success,
title: `Created labels successfully`,
});
}
if (loading) {
return <Skeleton width="100%" />;
if (errorCreatingLabels) {
const errorCreatingLabelsMessage = (
<>
{Array.from(errorCreatingLabelsDetails.getErrors()).map(
([label, error]) => (
<p key={label}>
Could not create label {label}: {error.error.message}
</p>
),
)}
</>
);
addAlert({
variant: AlertVariant.Failure,
title: `Could not create labels`,
message: errorCreatingLabelsMessage,
});
}
if (successDeletingLabels) {
addAlert({
variant: AlertVariant.Success,
title: `Deleted labels successfully`,
});
}
if (errorDeletingLabels) {
const errorDeletingLabelsMessage = (
<>
{Array.from(errorDeletingLabelsDetails.getErrors()).map(
([label, error]) => (
<p key={label}>
Could not delete label {label}: {error.error.message}
</p>
),
)}
</>
);
addAlert({
variant: AlertVariant.Failure,
title: `Could not delete labels`,
message: errorDeletingLabelsMessage,
});
}
if (
(successCreatingLabels ||
errorCreatingLabels ||
successDeletingLabels ||
errorDeletingLabels) &&
!loadingLabelChanges
) {
props.onComplete();
}
}, [
successCreatingLabels,
errorCreatingLabels,
successDeletingLabels,
errorDeletingLabels,
loadingLabelChanges,
addAlert,
props.onComplete,
errorCreatingLabelsDetails,
errorDeletingLabelsDetails,
]);
if (error) {
return <>Unable to get labels</>;
}
if (loading) {
return <Skeleton width="100%" />;
}
const onEditComplete = (newLabel: string) => {
if (newLabel === '') {
setNewLabel('');
setInvalidNewLabel(null);
return;
}
let invalidMessage: string = null;
let key: string = null;
let value: string = null;
const keyValue: string[] = newLabel.split('=');
if (keyValue.length === 2) {
key = keyValue[0].trim();
value = keyValue[1].trim();
if (key === '' || !/^[0-9A-Za-z/\-_.]+$/.test(key)) {
invalidMessage =
'Invalid label format, key must match ^[0-9A-Za-z/\\-_.]+=.+$';
}
if (value === '' || !/^[0-9A-Za-z/\-_.]+$/.test(value)) {
invalidMessage =
'Invalid label format, value must match ^[0-9A-Za-z/\\-_.]+=.+$';
}
} else {
invalidMessage = 'Invalid label format, must be key value separated by =';
}
useEffect(() => {
if (successCreatingLabels) {
addAlert({ variant: AlertVariant.Success, title: `Created labels successfully` });
}
if (errorCreatingLabels) {
let errorCreatingLabelsMessage = (<>{Array.from(errorCreatingLabelsDetails.getErrors()).map(([label, error]) => (<p key={label}>Could not create label {label}: {error.error.message}</p>))}</>)
addAlert({ variant: AlertVariant.Failure, title: `Could not create labels`, message: errorCreatingLabelsMessage });
}
if (successDeletingLabels) {
addAlert({ variant: AlertVariant.Success, title: `Deleted labels successfully` });
}
if (errorDeletingLabels) {
let errorDeletingLabelsMessage = (<>{Array.from(errorDeletingLabelsDetails.getErrors()).map(([label, error]) => (<p key={label}>Could not delete label {label}: {error.error.message}</p>))}</>)
addAlert({ variant: AlertVariant.Failure, title: `Could not delete labels`, message: errorDeletingLabelsMessage });
}
if ((successCreatingLabels || errorCreatingLabels || successDeletingLabels || errorDeletingLabels) && !loadingLabelChanges) {
props.onComplete();
}
}, [successCreatingLabels, errorCreatingLabels, successDeletingLabels, errorDeletingLabels]);
const onEditComplete = (newLabel: string) => {
if(newLabel === '') {
setNewLabel('');
setInvalidNewLabel(null);
return;
}
let invalidMessage: string = null;
let key: string = null;
let value: string = null;
const keyValue: string[] = newLabel.split('=');
if (keyValue.length === 2) {
key = keyValue[0].trim();
value = keyValue[1].trim();
if (key === "" || !/^[0-9A-Za-z/\-_.]+$/.test(key)) {
invalidMessage = 'Invalid label format, key must match ^[0-9A-Za-z/\\-_.]+=.+$'
}
if (value === "" || !/^[0-9A-Za-z/\-_.]+$/.test(value)) {
invalidMessage = 'Invalid label format, value must match ^[0-9A-Za-z/\\-_.]+=.+$';
}
} else {
invalidMessage = 'Invalid label format, must be key value separated by ='
}
if (labels.some(l => l.key === key && l.value === value)) {
invalidMessage = 'Key value already exists'
}
if (invalidMessage === null) {
setNewLabel('');
const newLabelObj: ImageLabel = { id: `${key}=${value}`, key, value, source_type: 'api', media_type: null };
setLabels(prev => [...prev, newLabelObj])
setInvalidNewLabel(null);
} else {
setInvalidNewLabel(invalidMessage);
setNewLabel(newLabel);
}
if (labels.some((l) => l.key === key && l.value === value)) {
invalidMessage = 'Key value already exists';
}
const removeLabel = (label: ImageLabel) => {
setLabels(prev => prev.filter(l => l.id !== label.id))
if (invalidMessage === null) {
setNewLabel('');
const newLabelObj: ImageLabel = {
id: `${key}=${value}`,
key,
value,
source_type: 'api',
media_type: null,
};
setLabels((prev) => [...prev, newLabelObj]);
setInvalidNewLabel(null);
} else {
setInvalidNewLabel(invalidMessage);
setNewLabel(newLabel);
}
};
const saveLabels = () => {
const [addedLabels, deletedLabels] = filterLabels(initialLabels, labels);
if (addedLabels.length > 0) {
createLabels(addedLabels);
}
if (deletedLabels.length > 0) {
deleteLabels(deletedLabels);
}
const removeLabel = (label: ImageLabel) => {
setLabels((prev) => prev.filter((l) => l.id !== label.id));
};
const saveLabels = () => {
const [addedLabels, deletedLabels] = filterLabels(initialLabels, labels);
if (addedLabels.length > 0) {
createLabels(addedLabels);
}
const isSaveButtonDisabled = () => {
const [addedLabels, deletedLabels] = filterLabels(initialLabels, labels);
return (addedLabels.length === 0 && deletedLabels.length === 0);
if (deletedLabels.length > 0) {
deleteLabels(deletedLabels);
}
};
const filterLabels = (initialLabels: ImageLabel[], updatedLabels: ImageLabel[]) => {
const addedLabels: ImageLabel[] = updatedLabels.filter(updatedLabel => !initialLabels.some(intitialLabel => intitialLabel.key===updatedLabel.key && intitialLabel.value===updatedLabel.value));
const deletedLabels: ImageLabel[] = initialLabels.filter(intitialLabel => !updatedLabels.some(updatedLabel => intitialLabel.key===updatedLabel.key && intitialLabel.value===updatedLabel.value));
return [addedLabels, deletedLabels];
}
const isSaveButtonDisabled = () => {
const [addedLabels, deletedLabels] = filterLabels(initialLabels, labels);
return addedLabels.length === 0 && deletedLabels.length === 0;
};
return (<>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>Read-only labels</DescriptionListTerm>
<DescriptionListDescription id='readonly-labels'>
{readonlyLabels?.length === 0 ? "No labels found" : readonlyLabels.map((label: ImageLabel) => (
<>
<Label key={label.key} className="label">
<span className="label-content">
{label.key} = {label.value}
</span>
</Label>{' '}
</>
))}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Mutable labels</DescriptionListTerm>
<DescriptionListDescription id='mutable-labels'>
{mutableLabels?.map((label: ImageLabel) => (
<Label key={label.id} className="label" onClose={() => { removeLabel(label) }}>
<span className="label-content">
{label.key}={label.value}
</span>
</Label>
))}
<EditableLabel
value={newLabel}
setValue={setNewLabel}
onEditComplete={onEditComplete}
invalid={invalidNewLabel !== null}
/>
<Conditional if={invalidNewLabel !== null}>
<div style={{ color: 'red' }}>
{invalidNewLabel}
</div>
</Conditional>
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<br />
<Button
key="cancel"
variant="primary"
onClick={props.onComplete}
>
Cancel
</Button>{' '}
<Button
key="modal-action-button"
variant="primary"
onClick={saveLabels}
isDisabled={isSaveButtonDisabled()}
>
Save Labels
</Button>
</>)
const filterLabels = (
initialLabels: ImageLabel[],
updatedLabels: ImageLabel[],
) => {
const addedLabels: ImageLabel[] = updatedLabels.filter(
(updatedLabel) =>
!initialLabels.some(
(intitialLabel) =>
intitialLabel.key === updatedLabel.key &&
intitialLabel.value === updatedLabel.value,
),
);
const deletedLabels: ImageLabel[] = initialLabels.filter(
(intitialLabel) =>
!updatedLabels.some(
(updatedLabel) =>
intitialLabel.key === updatedLabel.key &&
intitialLabel.value === updatedLabel.value,
),
);
return [addedLabels, deletedLabels];
};
return (
<>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>Read-only labels</DescriptionListTerm>
<DescriptionListDescription id="readonly-labels">
{readonlyLabels?.length === 0
? 'No labels found'
: readonlyLabels.map((label: ImageLabel) => (
<>
<Label key={label.key} className="label">
<span className="label-content">
{label.key} = {label.value}
</span>
</Label>{' '}
</>
))}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Mutable labels</DescriptionListTerm>
<DescriptionListDescription id="mutable-labels">
{mutableLabels?.map((label: ImageLabel) => (
<Label
key={label.id}
className="label"
onClose={() => {
removeLabel(label);
}}
>
<span className="label-content">
{label.key}={label.value}
</span>
</Label>
))}
<EditableLabel
value={newLabel}
setValue={setNewLabel}
onEditComplete={onEditComplete}
invalid={invalidNewLabel !== null}
/>
<Conditional if={invalidNewLabel !== null}>
<div style={{color: 'red'}}>{invalidNewLabel}</div>
</Conditional>
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<br />
<Button key="cancel" variant="primary" onClick={props.onComplete}>
Cancel
</Button>{' '}
<Button
key="modal-action-button"
variant="primary"
onClick={saveLabels}
isDisabled={isSaveButtonDisabled()}
>
Save Labels
</Button>
</>
);
}
interface EditableLabelsProps {
org: string;
repo: string;
digest: string;
onComplete?: () => void;
org: string;
repo: string;
digest: string;
onComplete?: () => void;
}

View File

@@ -18,8 +18,7 @@ import {
import {useCurrentUser} from 'src/hooks/UseCurrentUser';
import {useConvertAccount} from 'src/hooks/UseConvertAccount';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Avatar from 'src/components/Avatar';
interface ChangeAccountTypeModalProps {
@@ -33,7 +32,7 @@ export default function ChangeAccountTypeModal({
}: ChangeAccountTypeModalProps) {
const {user} = useCurrentUser();
const quayConfig = useQuayConfig();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const canConvert = user?.organizations?.length === 0;
const [convertStep, setConvertStep] = useState(canConvert ? 1 : 0); // Start at step 1 if can convert
const [accountType, setAccountType] = useState('organization'); // Default to organization

View File

@@ -12,6 +12,8 @@ import {
import {useCreateApplicationToken} from 'src/hooks/UseApplicationTokens';
import {IApplicationToken} from 'src/resources/UserResource';
import CredentialsModal from './CredentialsModal';
import {useUI} from 'src/contexts/UIContext';
import {AlertVariant} from 'src/contexts/UIContext';
interface CreateApplicationTokenModalProps {
isOpen: boolean;
@@ -27,14 +29,24 @@ export default function CreateApplicationTokenModal({
null,
);
const [error, setError] = useState('');
const {addAlert} = useUI();
const createTokenMutator = useCreateApplicationToken({
onSuccess: (data) => {
setCreatedToken(data.token);
setError('');
addAlert({
variant: AlertVariant.Success,
title: `Successfully created application token "${title}"`,
});
},
onError: (err) => {
setError(err.message);
addAlert({
variant: AlertVariant.Failure,
title: 'Failed to create application token',
message: err.message,
});
},
});

View File

@@ -25,6 +25,8 @@ import {addDisplayError} from 'src/resources/ErrorHandling';
import {IOrganization} from 'src/resources/OrganizationResource';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useCreateRepository} from 'src/hooks/UseCreateRepository';
import {useUI} from 'src/contexts/UIContext';
import {AlertVariant} from 'src/contexts/UIContext';
enum visibilityType {
PUBLIC = 'PUBLIC',
@@ -39,6 +41,7 @@ export default function CreateRepositoryModalTemplate(
}
const [err, setErr] = useState<string>();
const quayConfig = useQuayConfig();
const {addAlert} = useUI();
const [currentOrganization, setCurrentOrganization] = useState({
// For org scoped view, the name is set current org and for Repository list view,
@@ -53,10 +56,23 @@ export default function CreateRepositoryModalTemplate(
const {createRepository} = useCreateRepository({
onSuccess: () => {
addAlert({
variant: AlertVariant.Success,
title: `Successfully created repository ${currentOrganization.name}/${newRepository.name}`,
});
props.handleModalToggle();
},
onError: (error) => {
setErr(addDisplayError('Unable to create repository', error));
const errorMessage = addDisplayError(
'Unable to create repository',
error,
);
setErr(errorMessage);
addAlert({
variant: AlertVariant.Failure,
title: 'Unable to create repository',
message: errorMessage,
});
},
});

View File

@@ -13,8 +13,7 @@ import {
SelectOption,
} from '@patternfly/react-core';
import React from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useManageOrgSubscriptions} from 'src/hooks/UseMarketplaceSubscriptions';
interface OrgSubscriptionModalProps {
@@ -29,7 +28,7 @@ export default function OrgSubscriptionModal(props: OrgSubscriptionModalProps) {
const [selectedSku, setSelectedSku] = React.useState('');
const [menuIsOpen, setMenuIsOpen] = React.useState(false);
const [bindingQuantity, setBindingQuantity] = React.useState(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined,

View File

@@ -15,15 +15,14 @@ import {PlusIcon, TrashIcon} from '@patternfly/react-icons';
import React, {useEffect, useState} from 'react';
import DisplayModal from './robotAccountWizard/DisplayModal';
import {useRobotFederation} from 'src/hooks/useRobotFederation';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
function RobotFederationForm(props: RobotFederationFormProps) {
const [federationFormState, setFederationFormState] = useState<
RobotFederationFormEntryProps[]
>([]);
const alerts = useAlerts();
const {addAlert} = useUI();
const {robotFederationConfig, loading, fetchError, setRobotFederationConfig} =
useRobotFederation({
@@ -33,13 +32,13 @@ function RobotFederationForm(props: RobotFederationFormProps) {
setFederationFormState(
result.map((config) => ({...config, isExpanded: false})),
);
alerts.addAlert({
addAlert({
title: 'Robot federation config saved',
variant: AlertVariant.Success,
});
},
onError: (e) => {
alerts.addAlert({
addAlert({
title: e.error_message || 'Error saving federation config',
variant: AlertVariant.Failure,
});

View File

@@ -12,14 +12,13 @@ import NameAndDescription from 'src/components/modals/robotAccountWizard/NameAnd
import {addDisplayError} from 'src/resources/ErrorHandling';
import TeamView from './TeamView';
import {useCreateTeam} from 'src/hooks/UseTeams';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
export default function AddToTeam(props: AddToTeamProps) {
const [newTeamName, setNewTeamName] = useState('');
const [newTeamDescription, setNewTeamDescription] = useState('');
const [err, setErr] = useState<string>();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {createNewTeamHook} = useCreateTeam(props.orgName, {
onSuccess: () => {

View File

@@ -7,14 +7,13 @@ import {
PageSidebarBody,
} from '@patternfly/react-core';
import {Link, useLocation} from 'react-router-dom';
import {SidebarState} from 'src/atoms/SidebarState';
import {NavigationPath} from 'src/routes/NavigationPath';
import OrganizationsList from 'src/routes/OrganizationsList/OrganizationsList';
import RepositoriesList from 'src/routes/RepositoriesList/RepositoriesList';
import {useRecoilValue} from 'recoil';
import OverviewList from 'src/routes/OverviewList/OverviewList';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useCurrentUser} from 'src/hooks/UseCurrentUser';
import {useUI} from 'src/contexts/UIContext';
import ServiceKeys from 'src/routes/Superuser/ServiceKeys/ServiceKeys';
import ChangeLog from 'src/routes/Superuser/ChangeLog/ChangeLog';
import UsageLogs from 'src/routes/Superuser/UsageLogs/UsageLogs';
@@ -31,7 +30,7 @@ interface SideNavProps {
export function QuaySidebar() {
const location = useLocation();
const sidebarState = useRecoilValue(SidebarState);
const {isSidebarOpen} = useUI();
const quayConfig = useQuayConfig();
const {isSuperUser} = useCurrentUser();
@@ -178,7 +177,7 @@ export function QuaySidebar() {
</Nav>
);
if (sidebarState.isOpen) {
if (isSidebarOpen) {
return (
<PageSidebar className="page-sidebar" theme="dark">
<PageSidebarBody>{Navigation}</PageSidebarBody>

View File

@@ -0,0 +1,108 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
useMemo,
ReactNode,
} from 'react';
export enum AlertVariant {
Success = 'success',
Failure = 'danger',
}
export interface AlertDetails {
variant: AlertVariant;
title: string;
key?: string;
message?: string | ReactNode;
}
interface UIContextType {
// Sidebar state
isSidebarOpen: boolean;
toggleSidebar: () => void;
// Alert state
alerts: AlertDetails[];
addAlert: (alert: AlertDetails) => void;
removeAlert: (key: string) => void;
clearAllAlerts: () => void;
}
const UIContext = createContext<UIContextType | undefined>(undefined);
interface UIProviderProps {
children: React.ReactNode;
}
export function UIProvider({children}: UIProviderProps) {
// Sidebar state with localStorage persistence
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(() => {
const stored = localStorage.getItem('quay-sidebar-open');
return stored !== null ? stored === 'true' : true; // Default to open
});
// Alert state
const [alerts, setAlerts] = useState<AlertDetails[]>([]);
// Persist sidebar state to localStorage
useEffect(() => {
localStorage.setItem('quay-sidebar-open', String(isSidebarOpen));
}, [isSidebarOpen]);
const toggleSidebar = useCallback(() => {
setIsSidebarOpen((prev) => !prev);
}, []);
const addAlert = useCallback((alert: AlertDetails) => {
const alertWithKey = {
...alert,
key: alert.key ?? Math.random().toString(36).substring(7),
};
setAlerts((prev) => [...prev, alertWithKey]);
}, []);
const removeAlert = useCallback((key: string) => {
setAlerts((prev) => prev.filter((a) => a.key !== key));
}, []);
const clearAllAlerts = useCallback(() => {
setAlerts([]);
}, []);
const value: UIContextType = useMemo(
() => ({
isSidebarOpen,
toggleSidebar,
alerts,
addAlert,
removeAlert,
clearAllAlerts,
}),
[
isSidebarOpen,
toggleSidebar,
alerts,
addAlert,
removeAlert,
clearAllAlerts,
],
);
return <UIContext.Provider value={value}>{children}</UIContext.Provider>;
}
/**
* Custom hook to access UI context
* @returns UI context with sidebar state and future alert/theme state
*/
export function useUI(): UIContextType {
const context = useContext(UIContext);
if (context === undefined) {
throw new Error('useUI must be used within a UIProvider');
}
return context;
}

View File

@@ -1,21 +0,0 @@
import {useRecoilState} from 'recoil';
import {AlertDetails, alertState} from 'src/atoms/AlertState';
export function useAlerts() {
const [alerts, setAlerts] = useRecoilState(alertState);
const addAlert = (alert: AlertDetails) => {
if (alert.key == null) {
alert.key = Math.random().toString(36).substring(7);
}
setAlerts([...alerts, alert]);
};
const clearAllAlerts = () => {
setAlerts([]);
};
return {
addAlert,
clearAllAlerts,
};
}

View File

@@ -1,5 +1,5 @@
import {useForm} from 'react-hook-form';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
import {
createServiceKey,
CreateServiceKeyRequest,

View File

@@ -2,7 +2,7 @@ import {useState} from 'react';
import {useForm} from 'react-hook-form';
import {MirroringFormData} from 'src/routes/RepositoryDetails/Mirroring/types';
import {Entity, EntityKind} from 'src/resources/UserResource';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
// Default form values
const defaultFormValues: MirroringFormData = {

View File

@@ -1,5 +1,5 @@
import {useForm} from 'react-hook-form';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
import {
OAuthApplicationFormData,
defaultOAuthFormValues,

View File

@@ -18,8 +18,7 @@ import {
import {BulkOperationError, ResourceError} from 'src/resources/ErrorHandling';
import {useCurrentUser} from './UseCurrentUser';
import {IAvatar} from 'src/resources/OrganizationResource';
import {useAlerts} from './UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {useUI, AlertVariant} from 'src/contexts/UIContext';
import {addRepoPermissionToTeam} from 'src/resources/DefaultPermissionResource';
interface createNewTeamForNamespaceParams {
@@ -29,7 +28,7 @@ interface createNewTeamForNamespaceParams {
export function useCreateTeam(orgName, {onSuccess, onError}) {
const queryClient = useQueryClient();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
data: responseData,

View File

@@ -3,6 +3,7 @@ import {createRoot} from 'react-dom/client';
import reportWebVitals from './reportWebVitals';
import {RecoilRoot} from 'recoil';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {UIProvider} from './contexts/UIContext';
// Load App after patternfly so custom CSS that overrides patternfly doesn't require !important
import App from './App';
@@ -22,9 +23,11 @@ const root = createRoot(container);
root.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
<UIProvider>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</UIProvider>
</RecoilRoot>
</React.StrictMode>,
);

View File

@@ -3,11 +3,10 @@ import {
AlertActionCloseButton,
AlertGroup,
} from '@patternfly/react-core';
import {useRecoilState} from 'recoil';
import {AlertVariant, alertState} from 'src/atoms/AlertState';
import {useUI, AlertVariant} from 'src/contexts/UIContext';
export default function Alerts() {
const [alerts, setAlerts] = useRecoilState(alertState);
const {alerts, removeAlert} = useUI();
return (
<AlertGroup isToast isLiveRegion>
{alerts.map((alert) => (
@@ -19,7 +18,7 @@ export default function Alerts() {
actionClose={
<AlertActionCloseButton
onClose={() => {
setAlerts((prev) => prev.filter((a) => a.key !== alert.key));
removeAlert(alert.key);
}}
/>
}

View File

@@ -38,8 +38,7 @@ import {
TriggeredBuildDescription,
} from 'src/routes/RepositoryDetails/Builds/BuildHistory';
import Conditional from 'src/components/empty/Conditional';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {getErrorMessageFromUnknown} from 'src/resources/ErrorHandling';
import {useEffect, useState} from 'react';
import {
@@ -59,7 +58,7 @@ import {RepositoryBuildPhase} from 'src/resources/BuildResource';
export default function Build() {
const location = useLocation();
const navigate = useNavigate();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [poll, setPoll] = useState<boolean>(true);
const [showTimestamps, setShowTimestamps] = useState<boolean>(false);
const [modalOpen, setModalOpen] = useState<boolean>(false);

View File

@@ -16,6 +16,8 @@ import {useState} from 'react';
import FormError from 'src/components/errors/FormError';
import {addDisplayError} from 'src/resources/ErrorHandling';
import {useOrganizations} from 'src/hooks/UseOrganizations';
import {useUI} from 'src/contexts/UIContext';
import {AlertVariant} from 'src/contexts/UIContext';
interface Validation {
message: string;
@@ -40,6 +42,7 @@ export const CreateOrganizationModal = (
const [err, setErr] = useState<string>();
const {createOrganization} = useOrganizations();
const {addAlert} = useUI();
const handleNameInputChange = (value: string) => {
const regex = /^([a-z0-9]+(?:[._-][a-z0-9]+)*)$/;
@@ -77,9 +80,22 @@ export const CreateOrganizationModal = (
const createOrganizationHandler = async () => {
try {
await createOrganization(organizationName, organizationEmail);
addAlert({
variant: AlertVariant.Success,
title: `Successfully created organization ${organizationName}`,
});
props.handleModalToggle();
} catch (err) {
setErr(addDisplayError('Unable to create organization', err));
const errorMessage = addDisplayError(
'Unable to create organization',
err,
);
setErr(errorMessage);
addAlert({
variant: AlertVariant.Failure,
title: 'Unable to create organization',
message: errorMessage,
});
}
};

View File

@@ -15,8 +15,7 @@ import {CubeIcon} from '@patternfly/react-icons';
import {useAuthorizedApplications} from 'src/hooks/UseAuthorizedApplications';
import GenerateTokenAuthorizationModal from 'src/components/modals/GenerateTokenAuthorizationModal';
import TokenDisplayModal from 'src/components/modals/TokenDisplayModal';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {GlobalAuthState} from 'src/resources/AuthResource';
import {useQueryClient} from '@tanstack/react-query';
@@ -33,7 +32,7 @@ export default function AuthorizedApplicationsList() {
isRevoking,
isDeletingAssigned,
} = useAuthorizedApplications();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const queryClient = useQueryClient();
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);

View File

@@ -11,15 +11,14 @@ import {
useUpdateDefaultPermission,
} from 'src/hooks/UseDefaultPermissions';
import {repoPermissions} from './DefaultPermissionsList';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {titleCase} from 'src/libs/utils';
export default function DefaultPermissionsDropDown(
props: DefaultPermissionsDropdownProps,
) {
const [isOpen, setIsOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
setDefaultPermission,

View File

@@ -25,8 +25,7 @@ import {BulkDeleteModalTemplate} from 'src/components/modals/BulkDeleteModalTemp
import Conditional from 'src/components/empty/Conditional';
import {BulkOperationError, addDisplayError} from 'src/resources/ErrorHandling';
import RequestError from 'src/components/errors/RequestError';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import ErrorModal from 'src/components/errors/ErrorModal';
import {usePaginatedSortableTable} from '../../../../../hooks/usePaginatedSortableTable';
@@ -80,7 +79,7 @@ export default function DefaultPermissionsList(
>([]);
const [bulkDeleteModalIsOpen, setBulkDeleteModalIsOpen] = useState(false);
const [err, setError] = useState<string[]>();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const onSelectPermission = (
permission: IDefaultPermission,

View File

@@ -7,8 +7,7 @@ import {
MenuToggleElement,
} from '@patternfly/react-core';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {
IDefaultPermission,
useDeleteDefaultPermission,
@@ -18,7 +17,7 @@ export default function DeleteDefaultPermissionKebab(
props: DefaultPermissionsDropdownProps,
) {
const [isOpen, setIsOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
removeDefaultPermission,

View File

@@ -34,8 +34,7 @@ import Conditional from 'src/components/empty/Conditional';
import {useFetchTeams} from 'src/hooks/UseTeams';
import {repoPermissions} from 'src/routes/OrganizationsList/Organization/Tabs/DefaultPermissions/DefaultPermissionsList';
import {RepoPermissionDropdownItems} from 'src/routes/RepositoriesList/RobotAccountsList';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {validateTeamName} from 'src/libs/utils';
export default function CreatePermissionDrawer(
@@ -74,7 +73,7 @@ export default function CreatePermissionDrawer(
const {robots, isLoadingRobots} = useFetchRobotAccounts(props.orgName);
// Get teams
const {teams, isLoadingTeams} = useFetchTeams(props.orgName);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const permissionRadioButtons = (
<>

View File

@@ -11,8 +11,7 @@ import {
} from '@patternfly/react-core';
import {ExclamationCircleIcon} from '@patternfly/react-icons';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useCreateTeam} from 'src/hooks/UseTeams';
type validate = 'success' | 'error' | 'default';
@@ -23,7 +22,7 @@ export const CreateTeamModal = (props: CreateTeamModalProps): JSX.Element => {
const [validatedName, setValidatedName] = useState<validate>('default');
const [nameHelperText, setNameHelperText] = useState(props.nameHelperText);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const handleNameChange = (
_event: React.FormEvent<HTMLInputElement>,

View File

@@ -16,8 +16,7 @@ import {
import {getAccountTypeForMember} from 'src/libs/utils';
import {TrashIcon} from '@patternfly/react-icons';
import NameAndDescription from 'src/components/modals/robotAccountWizard/NameAndDescription';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import ToggleDrawer from 'src/components/ToggleDrawer';
const memberAndRobotColNames = {
@@ -30,7 +29,7 @@ export default function AddTeamMember(props: AddTeamMemberProps) {
const [perPage, setPerPage] = useState(20);
const [newRobotAccntName, setNewRobotAccntName] = useState('');
const [newRobotAccntDescription, setNewRobotAccntDescription] = useState('');
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {error, robots} = useFetchRobotAccounts(props.orgName);

View File

@@ -32,8 +32,7 @@ import AddTeamMember from './AddTeamMember';
import Review from './ReviewTeam';
import ReviewAndFinishFooter from './ReviewAndFinishFooter';
import {useAddRepoPermissionToTeam} from 'src/hooks/UseTeams';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
export const CreateTeamWizard = (props: CreateTeamWizardProps): JSX.Element => {
const [selectedRepoPerms, setSelectedRepoPerms] = useRecoilState(
@@ -47,7 +46,7 @@ export const CreateTeamWizard = (props: CreateTeamWizardProps): JSX.Element => {
const [deletedTeamMembers, setDeletedTeamMembers] = useState<ITeamMember[]>(
[],
);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
// Fetching repos
const {repos} = useRepositories(props.orgName);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import {Modal, ModalVariant, Button, Form} from '@patternfly/react-core';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useUI} from 'src/contexts/UIContext';
import {FormTextInput} from 'src/components/forms/FormTextInput';
import {useOAuthApplicationForm} from 'src/hooks/UseOAuthApplicationForm';
import {OAuthApplicationFormData} from './types';
@@ -8,7 +8,7 @@ import {OAuthApplicationFormData} from './types';
export default function CreateOAuthApplicationModal(
props: CreateOAuthApplicationModalProps,
) {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {control, errors, formValues, handleSubmit, isValid} =
useOAuthApplicationForm(

View File

@@ -23,8 +23,7 @@ import EntitySearch from 'src/components/EntitySearch';
import {Entity} from 'src/resources/UserResource';
import {GlobalAuthState} from 'src/resources/AuthResource';
import {OAUTH_SCOPES, OAuthScope} from '../types';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface GenerateTokenTabProps {
application: IOAuthApplication | null;
@@ -45,7 +44,7 @@ export default function GenerateTokenTab(props: GenerateTokenTabProps) {
const [isTokenDisplayModalOpen, setIsTokenDisplayModalOpen] = useState(false);
const {user} = useCurrentUser();
const quayConfig = useQuayConfig();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
// Initialize form with all scopes set to false
const defaultValues: GenerateTokenFormData = {};

View File

@@ -16,8 +16,7 @@ import {
IOAuthApplication,
useResetOAuthApplicationClientSecret,
} from 'src/hooks/UseOAuthApplications';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {ConfirmationModal} from 'src/components/modals/ConfirmationModal';
interface OAuthInformationTabProps {
@@ -29,7 +28,7 @@ interface OAuthInformationTabProps {
export default function OAuthInformationTab(props: OAuthInformationTabProps) {
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {resetOAuthApplicationClientSecretMutation} =
useResetOAuthApplicationClientSecret(

View File

@@ -11,8 +11,7 @@ import {
IOAuthApplication,
useUpdateOAuthApplication,
} from 'src/hooks/UseOAuthApplications';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {FormTextInput} from 'src/components/forms/FormTextInput';
import {OAuthApplicationFormData} from '../types';
@@ -23,7 +22,7 @@ interface SettingsTabProps {
}
export default function SettingsTab(props: SettingsTabProps) {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
control,

View File

@@ -7,8 +7,7 @@ import {
MenuToggleElement,
} from '@patternfly/react-core';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {
IOAuthApplication,
useDeleteOAuthApplication,
@@ -20,7 +19,7 @@ export default function OAuthApplicationActionsKebab(
) {
const [isOpen, setIsOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
removeOAuthApplication,

View File

@@ -18,8 +18,7 @@ import ManageOAuthApplicationDrawer from './ManageOAuthApplicationDrawer';
import Conditional from 'src/components/empty/Conditional';
import {BulkOperationError, addDisplayError} from 'src/resources/ErrorHandling';
import RequestError from 'src/components/errors/RequestError';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import ErrorModal from 'src/components/errors/ErrorModal';
import Empty from 'src/components/empty/Empty';
import {KeyIcon} from '@patternfly/react-icons';
@@ -44,7 +43,7 @@ export default function OAuthApplicationsList(
IOAuthApplication[]
>([]);
const [error, setError] = useState<string[]>([]);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
loading,

View File

@@ -1,9 +1,8 @@
import {Button, Spinner, Title} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Conditional from 'src/components/empty/Conditional';
import RequestError from 'src/components/errors/RequestError';
import {useAlerts} from 'src/hooks/UseAlerts';
import {
useCreateNamespaceAutoPrunePolicy,
useDeleteNamespaceAutoPrunePolicy,
@@ -31,7 +30,7 @@ export const shorthandTimeUnits = {
export default function AutoPruning(props: AutoPruning) {
const [policies, setPolicies] = useState([]);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const config = useQuayConfig();
const {
error,

View File

@@ -19,8 +19,7 @@ import {useOrganization} from 'src/hooks/UseOrganization';
import {useUpgradePlan} from 'src/hooks/UseUpgradePlan';
import {AxiosError} from 'axios';
import {useOrganizationSettings} from 'src/hooks/UseOrganizationSettings';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Alerts from 'src/routes/Alerts';
import MarketplaceDetails from './MarketplaceDetails';
@@ -31,7 +30,7 @@ type BillingInformationProps = {
export const BillingInformation = (props: BillingInformationProps) => {
const organizationName = props.organizationName;
const {user} = useCurrentUser();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
updateUser,
loading: userUpdateLoading,

View File

@@ -17,8 +17,7 @@ import {
import {useForm, Controller} from 'react-hook-form';
import moment from 'moment';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useCurrentUser, useUpdateUser} from 'src/hooks/UseCurrentUser';
import {useOrganization} from 'src/hooks/UseOrganization';
import {useOrganizationSettings} from 'src/hooks/UseOrganizationSettings';
@@ -65,7 +64,7 @@ export const GeneralSettings = (props: GeneralSettingsProps) => {
const {user, loading: isUserLoading} = useCurrentUser();
const {organization, isUserOrganization, loading} =
useOrganization(organizationName);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {updateOrgSettings} = useOrganizationSettings({
name: organizationName,

View File

@@ -12,8 +12,7 @@ import {
TextInput,
} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {
IProxyCacheConfig,
useCreateProxyCacheConfig,
@@ -41,7 +40,7 @@ export const ProxyCacheConfig = (props: ProxyCacheConfigProps) => {
const [proxyCacheConfig, setProxyCacheConfig] = useState<IProxyCacheConfig>(
defaultProxyCacheConfig,
);
const {addAlert, clearAllAlerts} = useAlerts();
const {addAlert, clearAllAlerts} = useUI();
const {fetchedProxyCacheConfig, isLoadingProxyCacheConfig} =
useFetchProxyCacheConfig(props.organizationName);

View File

@@ -23,8 +23,7 @@ import {PlusIcon} from '@patternfly/react-icons';
import {useEffect, useState} from 'react';
import {useForm, Controller} from 'react-hook-form';
import {FormTextInput} from 'src/components/forms/FormTextInput';
import {AlertVariant as AlertVariantState} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant as AlertVariantState, useUI} from 'src/contexts/UIContext';
import {useCurrentUser} from 'src/hooks/UseCurrentUser';
import {useSuperuserPermissions} from 'src/hooks/UseSuperuserPermissions';
import {
@@ -119,7 +118,7 @@ export const QuotaManagement = (props: QuotaManagementProps) => {
}>({});
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const {addAlert, clearAllAlerts} = useAlerts();
const {addAlert, clearAllAlerts} = useUI();
// Check if there's already a "Reject" limit to prevent adding duplicates
const hasRejectLimit = limits.some((limit) => limit.type === 'Reject');

View File

@@ -1,14 +1,13 @@
import {Alert, Button, Modal, ModalVariant} from '@patternfly/react-core';
import {useEffect} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useDeleteCollaborator} from 'src/hooks/UseMembers';
import {IMembers} from 'src/resources/MembersResource';
export default function CollaboratorsDeleteModal(
props: CollaboratorsDeleteModalProps,
) {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const deleteMsg =
'User will be removed from all teams and repositories under this organization in which they are a member or have permissions.';
const deleteAlert = (
@@ -27,6 +26,7 @@ export default function CollaboratorsDeleteModal(
variant: AlertVariant.Failure,
title: `Error deleting collaborator`,
});
props.toggleModal();
}
}, [errorDeleteCollaborator]);
@@ -36,6 +36,7 @@ export default function CollaboratorsDeleteModal(
variant: AlertVariant.Success,
title: `Successfully deleted collaborator`,
});
props.toggleModal();
}
}, [successDeleteCollaborator]);
@@ -55,7 +56,6 @@ export default function CollaboratorsDeleteModal(
removeCollaborator({
collaborator: props.collaborator.name,
});
props.toggleModal;
}}
data-testid={`${props.collaborator.name}-del-btn`}
>

View File

@@ -11,8 +11,7 @@ import {useFetchCollaborators} from 'src/hooks/UseMembers';
import {useEffect, useState} from 'react';
import {IMembers} from 'src/resources/MembersResource';
import {TrashIcon} from '@patternfly/react-icons';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
import CollaboratorsDeleteModal from './CollaboratorsDeleteModal';
import Conditional from 'src/components/empty/Conditional';
@@ -55,7 +54,7 @@ export default function CollaboratorsViewList(
const [selectedCollaborators, setSelectedCollaborators] = useState<
IMembers[]
>([]);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [collaboratorToBeDeleted, setCollaboratorToBeDeleted] =
useState<IMembers>();

View File

@@ -12,8 +12,7 @@ import {useEffect, useState} from 'react';
import MembersViewToolbar from './MembersViewToolbar';
import {useFetchMembers} from 'src/hooks/UseMembers';
import {IMemberTeams, IMembers} from 'src/resources/MembersResource';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {getTeamMemberPath} from 'src/routes/NavigationPath';
import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
import {usePaginatedSortableTable} from '../../../../../../hooks/usePaginatedSortableTable';
@@ -59,7 +58,7 @@ export default function MembersViewList(props: MembersViewListProps) {
const [selectedMembers, setSelectedMembers] = useState<IMembers[]>([]);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [searchParams] = useSearchParams();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
useEffect(() => {
if (error) {

View File

@@ -18,11 +18,10 @@ import {DesktopIcon} from '@patternfly/react-icons';
import React, {useState} from 'react';
import {Ref} from 'react';
import {useParams} from 'react-router-dom';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import EntitySearch from 'src/components/EntitySearch';
import Conditional from 'src/components/empty/Conditional';
import CreateRobotAccountModal from 'src/components/modals/CreateRobotAccountModal';
import {useAlerts} from 'src/hooks/UseAlerts';
import {
useAddMembersToTeam,
useFetchTeamMembersForOrg,
@@ -50,7 +49,7 @@ export default function AddNewTeamMemberDrawer(
const {robots, isLoadingRobots} = useFetchRobotAccounts(props.orgName);
// Get teams
const {teams} = useFetchTeams(props.orgName);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const creatorDefaultOptions = [
<React.Fragment key="creator">

View File

@@ -47,8 +47,7 @@ import {
} from '@patternfly/react-icons';
import {useParams} from 'react-router-dom';
import Empty from 'src/components/empty/Empty';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {
getAccountTypeForMember,
formatDate,
@@ -164,7 +163,7 @@ export default function ManageMembersList(props: ManageMembersListProps) {
const [selectedTeamMembers, setSelectedTeamMembers] = useState<ITeamMember[]>(
[],
);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [isEditing, setIsEditing] = useState(false);
const [teamDescr, setTeamDescr] = useState<string>();

View File

@@ -3,7 +3,7 @@ import {Button, Modal, ModalVariant, Spinner} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import Empty from 'src/components/empty/Empty';
import {CubesIcon} from '@patternfly/react-icons';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import SetRepoPermissionsToolbar from './SetRepoPermissionsForTeamToolbar';
import {
ITeamRepoPerms,
@@ -12,7 +12,6 @@ import {
} from 'src/hooks/UseTeams';
import {SetRepoPermForTeamRoleDropDown} from './SetRepoPermForTeamRoleDropDown';
import {formatDate} from 'src/libs/utils';
import {AlertVariant} from 'src/atoms/AlertState';
export const setRepoPermForTeamColumnNames = {
repoName: 'Repository',
@@ -50,7 +49,7 @@ export default function SetRepoPermissionForTeamModal(
[],
);
const [isKebabOpen, setKebabOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
useEffect(() => {
if (successUpdateRepoPerm) {

View File

@@ -6,8 +6,7 @@ import {
MenuToggle,
MenuToggleElement,
} from '@patternfly/react-core';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useUpdateTeamDetails} from 'src/hooks/UseTeams';
import {titleCase} from 'src/libs/utils';
@@ -19,7 +18,7 @@ export enum teamPermissions {
export function TeamsRoleDropDown(props: TeamsRoleDropDownProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
updateTeamDetails,

View File

@@ -22,8 +22,7 @@ import {BulkDeleteModalTemplate} from 'src/components/modals/BulkDeleteModalTemp
import {BulkOperationError, addDisplayError} from 'src/resources/ErrorHandling';
import ErrorModal from 'src/components/errors/ErrorModal';
import {getTeamMemberPath} from 'src/routes/NavigationPath';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import SetRepoPermissionForTeamModal from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal';
import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
import Conditional from 'src/components/empty/Conditional';
@@ -76,7 +75,7 @@ export default function TeamsViewList(props: TeamsViewListProps) {
const [deleteModalIsOpen, setDeleteModalIsOpen] = useState(false);
const [err, setIsError] = useState<string[]>();
const [searchParams] = useSearchParams();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [isSetRepoPermModalOpen, setIsSetRepoPermModalOpen] = useState(false);
const [repoPermForTeam, setRepoPermForTeam] = useState<string>('');
const [isDeleteModalForRowOpen, setIsDeleteModalForRowOpen] = useState(false);

View File

@@ -9,8 +9,7 @@ import {
Alert,
} from '@patternfly/react-core';
import {useChangeUserEmail} from 'src/hooks/UseUserActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface ChangeEmailModalProps {
isOpen: boolean;
@@ -21,7 +20,7 @@ interface ChangeEmailModalProps {
export default function ChangeEmailModal(props: ChangeEmailModalProps) {
const [newEmail, setNewEmail] = useState('');
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {changeEmail, isLoading} = useChangeUserEmail({
onSuccess: () => {

View File

@@ -9,8 +9,7 @@ import {
Alert,
} from '@patternfly/react-core';
import {useChangeUserPassword} from 'src/hooks/UseUserActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface ChangePasswordModalProps {
isOpen: boolean;
@@ -21,7 +20,7 @@ interface ChangePasswordModalProps {
export default function ChangePasswordModal(props: ChangePasswordModalProps) {
const [newPassword, setNewPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {changePassword, isLoading} = useChangeUserPassword({
onSuccess: () => {

View File

@@ -10,8 +10,7 @@ import {
} from '@patternfly/react-core';
import {useForm} from 'react-hook-form';
import {useCreateUser} from 'src/hooks/UseCreateUser';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface CreateUserModalProps {
isOpen: boolean;
@@ -28,7 +27,7 @@ interface CreateUserFormData {
export function CreateUserModal(props: CreateUserModalProps) {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
register,

View File

@@ -1,8 +1,7 @@
import {useState} from 'react';
import {Modal, ModalVariant, Button, Text, Alert} from '@patternfly/react-core';
import {useDeleteSingleOrganization} from 'src/hooks/UseOrganizationActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface DeleteOrganizationModalProps {
isOpen: boolean;
@@ -14,7 +13,7 @@ export default function DeleteOrganizationModal(
props: DeleteOrganizationModalProps,
) {
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {deleteOrganization, isLoading} = useDeleteSingleOrganization({
onSuccess: () => {

View File

@@ -1,8 +1,7 @@
import {useState} from 'react';
import {Modal, ModalVariant, Button, Text, Alert} from '@patternfly/react-core';
import {useDeleteUser} from 'src/hooks/UseUserActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface DeleteUserModalProps {
isOpen: boolean;
@@ -12,7 +11,7 @@ interface DeleteUserModalProps {
export default function DeleteUserModal(props: DeleteUserModalProps) {
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {deleteUser, isLoading} = useDeleteUser({
onSuccess: () => {

View File

@@ -9,8 +9,7 @@ import {
Alert,
} from '@patternfly/react-core';
import {useRenameOrganization} from 'src/hooks/UseOrganizationActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface RenameOrganizationModalProps {
isOpen: boolean;
@@ -23,7 +22,7 @@ export default function RenameOrganizationModal(
) {
const [newName, setNewName] = useState('');
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {renameOrganization, isLoading} = useRenameOrganization({
onSuccess: (oldName: string, newName: string) => {

View File

@@ -7,8 +7,7 @@ import {
AlertVariant as PFAlertVariant,
} from '@patternfly/react-core';
import {useSendRecoveryEmail} from 'src/hooks/UseUserActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface SendRecoveryEmailModalProps {
isOpen: boolean;
@@ -21,7 +20,7 @@ export default function SendRecoveryEmailModal(
) {
const [error, setError] = useState<string | null>(null);
const [successEmail, setSuccessEmail] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {sendRecovery, isLoading} = useSendRecoveryEmail({
onSuccess: (data) => {

View File

@@ -1,8 +1,7 @@
import {useState} from 'react';
import {Modal, ModalVariant, Button, Text, Alert} from '@patternfly/react-core';
import {useTakeOwnership} from 'src/hooks/UseOrganizationActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface TakeOwnershipModalProps {
isOpen: boolean;
@@ -13,7 +12,7 @@ interface TakeOwnershipModalProps {
export default function TakeOwnershipModal(props: TakeOwnershipModalProps) {
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const entityType = props.isUser ? 'user' : 'organization';
const {takeOwnership, isLoading} = useTakeOwnership({
@@ -46,8 +45,6 @@ export default function TakeOwnershipModal(props: TakeOwnershipModalProps) {
takeOwnership(props.organizationName);
};
const entityType = props.isUser ? 'user' : 'organization';
return (
<Modal
title="Take Ownership"

View File

@@ -1,8 +1,7 @@
import {useState} from 'react';
import {Modal, ModalVariant, Button, Text, Alert} from '@patternfly/react-core';
import {useToggleUserStatus} from 'src/hooks/UseUserActions';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
interface ToggleUserStatusModalProps {
isOpen: boolean;
@@ -15,14 +14,15 @@ export default function ToggleUserStatusModal(
props: ToggleUserStatusModalProps,
) {
const [error, setError] = useState<string | null>(null);
const {addAlert} = useAlerts();
const action = props.currentlyEnabled ? 'disabled' : 'enabled';
const {addAlert} = useUI();
const action = props.currentlyEnabled ? 'Disable' : 'Enable';
const actionLower = action.toLowerCase();
const {toggleStatus, isLoading} = useToggleUserStatus({
onSuccess: () => {
addAlert({
variant: AlertVariant.Success,
title: `Successfully ${action} user ${props.username}`,
title: `Successfully ${actionLower}d user ${props.username}`,
});
handleClose();
},
@@ -32,9 +32,7 @@ export default function ToggleUserStatusModal(
setError(errorMessage);
addAlert({
variant: AlertVariant.Failure,
title: `Failed to ${
action === 'disabled' ? 'disable' : 'enable'
} user ${props.username}`,
title: `Failed to ${actionLower} user ${props.username}`,
message: errorMessage,
});
},
@@ -50,9 +48,6 @@ export default function ToggleUserStatusModal(
toggleStatus(props.username, !props.currentlyEnabled);
};
const action = props.currentlyEnabled ? 'Disable' : 'Enable';
const actionLower = action.toLowerCase();
return (
<Modal
title={`${action} User`}

View File

@@ -50,8 +50,7 @@ import {
import {useRobotRepoPermissions} from 'src/hooks/useRobotAccounts';
import RobotTokensModal from 'src/components/modals/RobotTokensModal';
import {SearchState} from 'src/components/toolbar/SearchTypes';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {RobotFederationModal} from 'src/components/modals/RobotFederationModal';
import {usePaginatedSortableTable} from '../../hooks/usePaginatedSortableTable';
@@ -113,7 +112,7 @@ export default function RobotAccountsList(props: RobotAccountsListProps) {
const [isRobotFederationModalOpen, setRobotFederationModalOpen] =
useState<boolean>(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {robotAccountsForOrg} = useRobotAccounts({
name: props.organizationName,

View File

@@ -14,8 +14,7 @@ import BuildTriggerDescription from './BuildTriggerDescription';
import Conditional from 'src/components/empty/Conditional';
import {useState} from 'react';
import {useStartBuild} from 'src/hooks/UseBuilds';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useSourceRefs} from 'src/hooks/UseBuildTriggers';
import TypeAheadSelect from 'src/components/TypeAheadSelect';
import {isNullOrUndefined} from 'src/libs/utils';
@@ -25,7 +24,7 @@ export default function ManuallyStartTrigger(props: ManuallyStartTriggerProps) {
const [commit, setCommit] = useState<string>('');
const [ref, setRef] = useState<SourceRef>({name: '', kind: null});
const [isValid, setIsValid] = useState<boolean>(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {refs, isLoading, isError, error} = useSourceRefs(
org,
repo,

View File

@@ -12,12 +12,11 @@ import {
import {DesktopIcon} from '@patternfly/react-icons';
import React from 'react';
import {useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import EntitySearch from 'src/components/EntitySearch';
import FileUpload from 'src/components/FileUpload';
import Conditional from 'src/components/empty/Conditional';
import CreateRobotAccountModal from 'src/components/modals/CreateRobotAccountModal';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useStartDockerfileBuild} from 'src/hooks/UseBuilds';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useRepository, useTransitivePermissions} from 'src/hooks/UseRepository';
@@ -37,7 +36,7 @@ export default function DockerfileUploadBuild(
const [privateRepo, setPrivatRepo] = useState<string>();
const [selectedRobot, setSelectedRobot] = useState<string>(null);
const [isCreateRobotModalOpen, setIsCreateRobotModalOpen] = useState(false);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {teams} = useFetchTeams(props.org);
const [org, repo] = privateRepo?.split('/') ?? [null, null];
const {repoDetails} = useRepository(org, repo);

View File

@@ -1,12 +1,11 @@
import {Button, Modal, ModalVariant} from '@patternfly/react-core';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useDeleteBuildTrigger} from 'src/hooks/UseBuildTriggers';
export default function BuildTriggerDeleteModal(
props: BuildTriggerDeleteModalProps,
) {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {deleteTrigger} = useDeleteBuildTrigger(
props.org,
props.repo,

View File

@@ -12,8 +12,7 @@ import ContextStep from './BuildTriggerSetupWizardContext';
import RobotAccounts from './BuildTriggerSetupWizardRobotAccounts';
import ReviewAndFinishProps from './BuildTriggerSetupWizardReviewAndFinish';
import {useActivateBuildTrigger} from 'src/hooks/UseBuildTriggers';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import SelectOrganization from './BuildTriggerSetupWizardOrganization';
import {isNullOrUndefined} from 'src/libs/utils';
import HostedRepository from './BuildTriggerSetupWizardHostedRepository';
@@ -45,7 +44,7 @@ export default function BuildTriggerSetupWizard(
repoUrl,
!isNullOrUndefined(repoUrl) && repoUrl !== '' && !isCustomGit,
);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {activateTrigger} = useActivateBuildTrigger(
props.org,
props.repo,

View File

@@ -1,12 +1,11 @@
import {Button, Modal, ModalVariant} from '@patternfly/react-core';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useToggleBuildTrigger} from 'src/hooks/UseBuildTriggers';
export default function BuildTriggerToggleModal(
props: BuildTriggerToggleModalProps,
) {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {toggleTrigger} = useToggleBuildTrigger(
props.org,
props.repo,

View File

@@ -1,13 +1,12 @@
import {Td, Tr} from '@patternfly/react-table';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Conditional from 'src/components/empty/Conditional';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useDeleteBuildTrigger} from 'src/hooks/UseBuildTriggers';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
export default function InactiveTrigger(props: InactiveTriggerProps) {
const config = useQuayConfig();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {deleteTrigger} = useDeleteBuildTrigger(
props.org,
props.repo,

View File

@@ -24,8 +24,7 @@ import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useQuayState} from 'src/hooks/UseQuayState';
import {RepositoryDetails} from 'src/resources/RepositoryResource';
import axios from 'src/libs/axios';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import React from 'react';
@@ -91,7 +90,7 @@ export default function Information(props: InformationProps) {
const {organization, repository, repoDetails} = props;
const config = useQuayConfig();
const {inReadOnlyMode} = useQuayState();
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const queryClient = useQueryClient();
const [description, setDescription] = useState(

View File

@@ -21,7 +21,7 @@ import {
} from '@patternfly/react-core';
import {DesktopIcon, UsersIcon} from '@patternfly/react-icons';
import {useRepository} from 'src/hooks/UseRepository';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useUI} from 'src/contexts/UIContext';
import FormError from 'src/components/errors/FormError';
import {useFetchRobotAccounts} from 'src/hooks/useRobotAccounts';
import {useFetchTeams} from 'src/hooks/UseTeams';
@@ -40,7 +40,7 @@ export const Mirroring: React.FC<MirroringProps> = ({namespace, repoName}) => {
errorLoadingRepoDetails,
isLoading: isLoadingRepo,
} = useRepository(namespace, repoName);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const queryClient = useQueryClient();
// Initialize form hook

View File

@@ -18,7 +18,7 @@ import {FormTextInput} from 'src/components/forms/FormTextInput';
import {FormCheckbox} from 'src/components/forms/FormCheckbox';
import EntitySearch from 'src/components/EntitySearch';
import {Entity} from 'src/resources/UserResource';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
import {
MirroringConfigResponse,
getMirrorConfig,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
import CreateRobotAccountModal from 'src/components/modals/CreateRobotAccountModal';
import {CreateTeamModal} from 'src/routes/OrganizationsList/Organization/Tabs/DefaultPermissions/createPermissionDrawer/CreateTeamModal';
import {Entity} from 'src/resources/UserResource';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import {Divider, Button} from '@patternfly/react-core';
import {StatusDisplay} from 'src/components/StatusDisplay';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant} from 'src/contexts/UIContext';
import {
MirroringConfigResponse,
getMirrorConfig,

View File

@@ -15,13 +15,12 @@ import {
} from '@patternfly/react-core';
import {useEffect, useRef, useState} from 'react';
import {useLocation, useNavigate, useSearchParams} from 'react-router-dom';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {QuayBreadcrumb} from 'src/components/breadcrumb/Breadcrumb';
import Conditional from 'src/components/empty/Conditional';
import ErrorBoundary from 'src/components/errors/ErrorBoundary';
import RequestError from 'src/components/errors/RequestError';
import CreateRobotAccountModal from 'src/components/modals/CreateRobotAccountModal';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import {useRepository} from 'src/hooks/UseRepository';
import {useFetchTeams} from 'src/hooks/UseTeams';
@@ -73,7 +72,7 @@ export default function RepositoryDetails() {
);
const [isCreateRobotModalOpen, setIsCreateRobotModalOpen] = useState(false);
const [selectedEntity, setSelectedEntity] = useState<Entity>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [err, setErr] = useState<string>();
const drawerRef = useRef<HTMLDivElement>();

View File

@@ -1,9 +1,8 @@
import {Button, Spinner, Title} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import Conditional from 'src/components/empty/Conditional';
import RequestError from 'src/components/errors/RequestError';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useNamespaceAutoPrunePolicies} from 'src/hooks/UseNamespaceAutoPrunePolicies';
import {useOrganization} from 'src/hooks/UseOrganization';
import {
@@ -23,7 +22,7 @@ import {getErrorMessageFromUnknown} from 'src/resources/ErrorHandling';
export default function RepositoryAutoPruning(props: RepositoryAutoPruning) {
const [policies, setPolicies] = useState([]);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {organization} = useOrganization(props.organizationName);
const {user} = useCurrentUser();
const config = useQuayConfig();

View File

@@ -3,13 +3,12 @@ import {TagAction, TagEntry} from './types';
import ManifestDigest from 'src/components/ManifestDigest';
import {useEffect, useState} from 'react';
import {usePermanentlyDeleteTag} from 'src/hooks/UseTags';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {InfoCircleIcon} from '@patternfly/react-icons';
export default function PermanentlyDeleteTag(props: RestoreTagProps) {
const {tagEntry} = props;
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const {permanentlyDeleteTag, success, error} = usePermanentlyDeleteTag(
props.org,

View File

@@ -3,12 +3,11 @@ import {TagAction, TagEntry} from './types';
import {ReactElement, useEffect, useState} from 'react';
import {Alert, Button, Label, Modal} from '@patternfly/react-core';
import {useRestoreTag} from 'src/hooks/UseTags';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
export default function RestoreTag(props: RestoreTagProps) {
const {tagEntry, org, repo} = props;
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const {restoreTag, success, error} = useRestoreTag(org, repo);

View File

@@ -11,8 +11,7 @@ import './Tags.css';
import {isNullOrUndefined} from 'src/libs/utils';
import Conditional from 'src/components/empty/Conditional';
import {useDeleteTag} from 'src/hooks/UseTags';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertDetails, AlertVariant} from 'src/atoms/AlertState';
import {AlertDetails, AlertVariant, useUI} from 'src/contexts/UIContext';
export interface ModalOptions {
isOpen: boolean;
@@ -26,7 +25,7 @@ export function DeleteModal(props: ModalProps) {
errorDeleteTags,
errorDeleteTagDetails,
} = useDeleteTag(props.org, props.repo);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const isReadonly: boolean = props.repoDetails?.state !== 'NORMAL';
useEffect(() => {

View File

@@ -6,14 +6,13 @@ import {
Title,
} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useCreateTag} from 'src/hooks/UseTags';
import {isNullOrUndefined} from 'src/libs/utils';
export default function AddTagModal(props: AddTagModalProps) {
const [value, setValue] = useState('');
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {createTag, successCreateTag, errorCreateTag} = useCreateTag(
props.org,
props.repo,

View File

@@ -14,14 +14,13 @@ import {
TimePicker,
} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
import {useSetExpiration} from 'src/hooks/UseTags';
import {formatDate, isNullOrUndefined} from 'src/libs/utils';
export default function EditExpirationModal(props: EditExpirationModalProps) {
const [date, setDate] = useState<Date>(null);
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const {
setExpiration,
successSetExpiration,

View File

@@ -39,6 +39,7 @@ import {OAuthError} from 'src/routes/OAuthCallback/OAuthError';
import SystemStatusBanner from 'src/components/SystemStatusBanner';
import {GlobalMessages} from 'src/components/GlobalMessages';
import {LoadingPage} from 'src/components/LoadingPage';
import {useUI} from 'src/contexts/UIContext';
// Lazy load route components for better performance
const OrganizationsList = lazy(
@@ -212,9 +213,16 @@ export function StandaloneMain() {
const quayConfig = useQuayConfig();
const {loading, error} = useCurrentUser();
const location = useLocation();
const {clearAllAlerts} = useUI();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Clear alerts when navigating to a different route
useEffect(() => {
clearAllAlerts();
}, [location.pathname, clearAllAlerts]);
const toggleDrawer = () => {
setIsDrawerOpen((prev) => !prev);
};

View File

@@ -12,7 +12,7 @@ import {
HelperTextItem,
} from '@patternfly/react-core';
import {useCreateServiceKey} from 'src/hooks/UseCreateServiceKey';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useUI} from 'src/contexts/UIContext';
import FormError from 'src/components/errors/FormError';
interface CreateServiceKeyFormProps {
@@ -26,7 +26,7 @@ export const CreateServiceKeyForm: React.FC<CreateServiceKeyFormProps> = ({
onClose,
onSuccess,
}) => {
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const [error, setError] = React.useState<string | null>(null);
const formHook = useCreateServiceKey(addAlert, setError, () => {

View File

@@ -10,8 +10,7 @@ import {
} from '@patternfly/react-core';
import {exportLogs} from 'src/hooks/UseUsageLogs';
import {useAlerts} from 'src/hooks/UseAlerts';
import {AlertVariant} from 'src/atoms/AlertState';
import {AlertVariant, useUI} from 'src/contexts/UIContext';
export default function ExportLogsModal(props: ExportLogsModalProps) {
const [isModalOpen, setIsModalOpen] = React.useState(false);
@@ -22,7 +21,7 @@ export default function ExportLogsModal(props: ExportLogsModalProps) {
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
setIsModalOpen(!isModalOpen);
};
const {addAlert} = useAlerts();
const {addAlert} = useUI();
const exportLogsClick = (callback: string) => {
return exportLogs(