hooks#useToggle TypeScript Examples
The following examples show how to use
hooks#useToggle.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ReadMore.tsx From one-platform with MIT License | 6 votes |
ReadMore = ({
children,
component,
limit = 300,
showMoreText = '... Read more',
showLessText = 'Show Less',
...props
}: Props & TextProps): JSX.Element => {
const [isReadMore, setReadMore] = useToggle(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Component: any = component || Fragment;
const text = children;
if (typeof text !== 'string' && !Array.isArray(text)) {
throw Error('String required');
}
const isReadMoreHidden = text.length <= limit;
return (
<Component {...props}>
{isReadMore ? text.slice(0, limit) : text}
{!isReadMoreHidden && (
<Button
onClick={setReadMore.toggle}
variant="link"
isInline
className="pf-u-ml-sm pf-u-font-size-sm"
>
{isReadMore ? showMoreText : showLessText}
</Button>
)}
</Component>
);
}
Example #2
Source File: Select.tsx From one-platform with MIT License | 6 votes |
Select = ({ children, onSelect, ...selectProps }: Props): JSX.Element => {
const [isOpen, setIsOpen] = useToggle();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPfeSelect = (...args: any) => {
// eslint-disable-next-line prefer-spread
onSelect?.apply(null, args);
setIsOpen.off();
};
return (
<PfeSelect {...selectProps} onSelect={onPfeSelect} isOpen={isOpen} onToggle={setIsOpen.toggle}>
{children}
</PfeSelect>
);
}
Example #3
Source File: ReadMore.tsx From one-platform with MIT License | 6 votes |
ReadMore = ({
children,
component,
limit = 300,
...props
}: Props & TextProps): JSX.Element => {
const [isReadMore, setReadMore] = useToggle(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Component: any = component || Text;
const text = children;
if (typeof text !== 'string') {
throw Error('String required');
}
const isReadMoreHidden = text.length <= limit;
return (
<Component {...props}>
{isReadMore ? text.slice(0, limit) : text}
{!isReadMoreHidden && (
<>
{isReadMore && '... '}
<Button onClick={setReadMore.toggle} variant="link" isInline>
{isReadMore ? 'Read more' : ' Show less'}
</Button>
</>
)}
</Component>
);
}
Example #4
Source File: AsyncSelect.tsx From one-platform with MIT License | 5 votes |
AsyncSelect = ({
render,
onSelect,
customFilter,
onTypeaheadInputChanged,
...selectProps
}: Props): JSX.Element => {
const [isOpen, setIsOpen] = useToggle();
const [options, setOptions] = useState<ReactElement<any, string | JSXElementConstructor<any>>[]>(
[]
);
const [typeAhead, setTypeAhead] = useState('');
useEffect(() => {
if (!isOpen) {
setTypeAhead('');
setOptions([]);
return;
}
setOptions(LOADING);
render(typeAhead)
.then((loadedOptions) => {
setOptions(loadedOptions);
})
.catch(() => {
setOptions([
<SelectOption
key="option-error"
value="Failed to fetch request"
isPlaceholder
isDisabled
/>,
]);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typeAhead, isOpen]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPfeSelect = (...args: any) => {
// eslint-disable-next-line prefer-spread
onSelect?.apply(null, args);
setIsOpen.off();
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPfeTypeAheadChange = (value: string) => {
setTypeAhead(value);
// eslint-disable-next-line prefer-spread
if (onTypeaheadInputChanged) onTypeaheadInputChanged(value);
};
const onFilter = (a: ChangeEvent<HTMLInputElement> | null, value: string) => {
if (!value) {
return options;
}
if (!customFilter) return options;
return options.filter((child) => customFilter(child));
};
return (
<PfeSelect
{...selectProps}
onSelect={onPfeSelect}
isOpen={isOpen}
onToggle={setIsOpen.toggle}
onTypeaheadInputChanged={onPfeTypeAheadChange}
onFilter={customFilter && onFilter}
>
{options}
</PfeSelect>
);
}
Example #5
Source File: AppLayout.tsx From one-platform with MIT License | 5 votes |
AppLayout: FC = () => {
const { pathname } = useLocation();
const [isSidebarOpen, setIsSidebarOpen] = useToggle(true);
const { breadcrumbs } = useBreadcrumb();
const isBreadcrumbHidden = breadcrumbs.length === 0;
return (
<Page
mainContainerId="app-layout-page"
sidebar={<Sidebar isOpen={isSidebarOpen} />}
className={styles['app-layout']}
breadcrumb={
!isBreadcrumbHidden && (
<Breadcrumb className={styles['app-layout--breadcrumb']}>
<BreadcrumbItem>
<Button variant="link" className="pf-u-p-0" onClick={setIsSidebarOpen.toggle}>
<BarsIcon />
</Button>
</BreadcrumbItem>
<BreadcrumbItem to="/">One Platform</BreadcrumbItem>
<BreadcrumbItem>
{pathname === config.baseURL ? 'API Catalog Home' : <Link to="/">API Catalog</Link>}
</BreadcrumbItem>
{breadcrumbs.map(({ label, url }, index) => {
const isActive = index === breadcrumbs.length - 1;
return (
<BreadcrumbItem key={label}>
{isActive ? label : <Link to={url}>{label}</Link>}
</BreadcrumbItem>
);
})}
</Breadcrumb>
)
}
>
<PageSection
className={styles['app-layout--content']}
variant="light"
padding={{ default: 'noPadding' }}
>
<Outlet />
</PageSection>
</Page>
);
}
Example #6
Source File: SwaggerToolboxPage.tsx From one-platform with MIT License | 5 votes |
SwaggerToolboxPage = (): JSX.Element => {
const { envSlug } = useParams();
const navigate = useNavigate();
const { pathname } = useLocation();
const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
const [isDecodingFile, setIsDecodingFile] = useToggle();
const schema = schemaData?.fetchAPISchema?.schema;
const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
const file = schemaData?.fetchAPISchema?.file;
useRegisterRecentVisit({
isLoading: isLoading || isDecodingFile,
log: useMemo(
() => ({
title: schema?.name || '',
tool: 'swagger',
url: pathname,
id: namespaceSlug as string,
envSlug: envSlug as string,
}),
[pathname, namespaceSlug, schema?.name, envSlug]
),
onRemoveId: namespaceSlug,
});
const schemaFile = useMemo(() => {
if (file) {
try {
setIsDecodingFile.on();
const data = yaml.load(window.atob(file));
return data as object;
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to parse file!!',
});
} finally {
setIsDecodingFile.off();
}
}
return '';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [file]);
if (isLoading || isDecodingFile) {
return (
<Bullseye>
<Spinner size="xl" />
</Bullseye>
);
}
if (!file) {
return (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={CubeIcon} />
<Title headingLevel="h4" size="lg">
Sorry, Couldn't find this API
</Title>
<Button variant="primary" onClick={() => navigate('/apis')}>
Go Back
</Button>
</EmptyState>
</Bullseye>
);
}
return <SwaggerUI spec={schemaFile} tryItOutEnabled />;
}
Example #7
Source File: APICUDPage.tsx From one-platform with MIT License | 4 votes |
APICUDPage = () => {
const [wizardStep, setWizardStep] = useState(1);
const { handleDynamicCrumbs } = useBreadcrumb();
const userInfo = opcBase.auth?.getUserInfo();
const navigate = useNavigate();
const { slug } = useParams();
const gqlClient = useURQL();
const isUpdate = Boolean(slug);
const [isDeleteConfirmationOpen, setIsDeleteConfirmationOpen] = useToggle();
// gql queries
const [createNsState, createNamespace] = useCreateNamespace();
const [, updateANamespace] = useUpdateNamespace();
const [deleteNamespaceState, deleteANamespace] = useDeleteANamespace();
const { isLoading: isNamespaceLoading, data: nsData } = useGetANamespaceBySlug({ slug });
const namespace = nsData?.getNamespaceBySlug;
const id = namespace?.id;
const formMethod = useForm<FormData>({
defaultValues: FORM_DEFAULT_VALUE,
mode: 'onBlur',
reValidateMode: 'onBlur',
resolver: yupResolver(
wizardValidationSchemas[wizardStep as keyof typeof wizardValidationSchemas]
),
});
const { handleSubmit, reset } = formMethod;
// effect for breadcrumb data
useEffect(() => {
if (!isNamespaceLoading && isUpdate && namespace?.name && namespace?.slug) {
handleDynamicCrumbs({
'api-name': { label: namespace.name, url: `/apis/${namespace.slug}` },
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNamespaceLoading, namespace?.name, namespace?.slug, isUpdate]);
/**
* This effect validated whether user has edit access if not move them to explore
* Checks user is in owner list or in createdBy
*/
useEffect(() => {
if (!isNamespaceLoading && isUpdate && namespace) {
const userUuid = userInfo?.rhatUUID;
const isApiCreatedUser = userUuid === (namespace?.createdBy as UserRoverDetails)?.rhatUUID;
const isOwner =
namespace?.owners.findIndex(
(owner) => owner.group === ApiEmailGroup.USER && owner.user.rhatUUID === userUuid
) !== -1;
const hasEditAccess = isApiCreatedUser || isOwner;
if (!hasEditAccess) navigate('/apis');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNamespaceLoading, namespace, isUpdate, userInfo?.rhatUUID]);
/**
* In update mode the form is prefilled with API config data
*/
useEffect(() => {
if (!isNamespaceLoading && isUpdate && namespace) {
const owners = namespace.owners.map((owner) => ({
group: owner.group,
mid: owner.group === ApiEmailGroup.USER ? owner?.user?.rhatUUID : owner?.email,
email: owner.group === ApiEmailGroup.USER ? owner?.user?.mail : owner?.email,
}));
reset({
...namespace,
owners,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNamespaceLoading, namespace, isUpdate]);
const isLastStep = wizardStep === MAX_WIZARD_STEPS;
const formatFormData = ({ id: nsId, slug: nSlug, ...data }: FormData) => {
return {
...data,
owners: data.owners.map(({ group, mid }) => ({ group, mid })),
schemas: data.schemas.map((schema) => ({
...schema,
environments: schema.environments.map(({ slug: eSlug, ...env }) => ({
...env,
headers: (env?.headers || [])
.filter(({ id: hId, key, value }) => (hId && key) || (key && value))
.map(({ id: hID, key, value }) => (hID ? { id: hID, key } : { key, value })),
})),
})),
};
};
const handleCreateNamespace = async (data: FormData) => {
const payload = formatFormData(data) as CreateNamespaceType;
try {
const res = await createNamespace({ payload });
if (res.error) {
window.OpNotification.danger({
subject: 'Failed to create API',
body: res.error?.message,
});
return;
}
navigate(`/apis/${res.data?.createNamespace.slug}`);
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to create API',
});
}
};
const handleUpdateNamespace = async (data: FormData) => {
const payload = formatFormData(data) as CreateNamespaceType;
delete (payload as any).createdBy;
try {
const res = await updateANamespace({ payload, id: id as string });
if (res.error) {
window.OpNotification.danger({
subject: 'Failed to update API',
body: res.error?.message,
});
return;
}
navigate(`/apis/${res.data?.updateNamespace.slug}`);
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to update API',
});
}
};
const handleSchemaValidation = async ({ envSlug, ...config }: HandleSchemaValidationArg) => {
try {
const res = await gqlClient
.query<UseGetAPISchemaFileQuery, UseGetAPISchemaFileVariable>(GET_API_SCHEMA_FILE, {
config,
envSlug,
})
.toPromise();
return res.data?.fetchAPISchema;
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to fetch schema',
});
}
return undefined;
};
const handleApiDelete = async (): Promise<void> => {
if (deleteNamespaceState.fetching) return;
const res = await deleteANamespace({ id: id as string });
if (res.error) {
window.OpNotification.danger({
subject: `Failed to delete API`,
body: res.error?.message,
});
} else {
navigate('/apis');
}
};
const onFormSubmit = (data: FormData) => {
if (wizardStep < MAX_WIZARD_STEPS) {
setWizardStep((state) => state + 1);
return;
}
if (isUpdate) {
handleUpdateNamespace(data);
} else {
handleCreateNamespace(data);
}
};
const onPrev = () => {
if (wizardStep > 1) {
setWizardStep((state) => state - 1);
}
};
const onSearchOwners = async (search: string): Promise<JSX.Element[]> => {
if (!search || search.length < 3) {
return [
<SelectOption key="no-result" value="Please type atleast 3 characters" isPlaceholder />,
];
}
try {
const res = await gqlClient.query<UserSearchQuery>(GET_USERS_QUERY, { search }).toPromise();
const options = (res.data?.searchRoverUsers || []).map((owner) => (
<SelectOption
key={`user:${owner.mail}-owner-${owner.rhatUUID}`}
value={{
...owner,
toString: () => owner.cn,
}}
description={owner.mail}
/>
));
return options;
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to search for users',
});
}
return [];
};
if (isUpdate && isNamespaceLoading) {
return (
<Bullseye>
<Spinner size="xl" />
</Bullseye>
);
}
return (
<PageSection
isCenterAligned
isWidthLimited
style={{ backgroundColor: 'var(--pf-global--BackgroundColor--light-300)' }}
className="pf-u-h-100 pf-u-pb-4xl"
>
<Form
onSubmit={handleSubmit(onFormSubmit)}
style={{ maxWidth: '1080px', margin: 'auto' }}
autoComplete="off"
>
<FormProvider {...formMethod}>
<Stack hasGutter>
{/* Top Stepper */}
<StackItem>
<Card>
<CardBody>
<ProgressStepper isCenterAligned>
{wizardStepDetails.map(({ title }, index) => (
<ProgressStep
variant={wizardStep <= index ? 'pending' : 'success'}
id={`wizard-step-icon-${index}`}
key={`wizard-step-icon-${index + 1}`}
titleId={`wizard-step-icon-${index}`}
aria-label={title}
isCurrent={wizardStep === index + 1}
>
{title}
</ProgressStep>
))}
</ProgressStepper>
</CardBody>
</Card>
</StackItem>
{/* Form Steps */}
<StackItem>
<CSSTransition in={wizardStep === 1} timeout={200} classNames="fade-in" unmountOnExit>
<APIBasicDetailsForm onSearchOwners={onSearchOwners} />
</CSSTransition>
</StackItem>
<StackItem>
<CSSTransition in={wizardStep === 2} timeout={200} classNames="fade-in" unmountOnExit>
<APISchemaForm
handleSchemaValidation={handleSchemaValidation}
isUpdate={isUpdate}
/>
</CSSTransition>
</StackItem>
<StackItem>
<CSSTransition in={wizardStep === 3} timeout={200} classNames="fade-in" unmountOnExit>
<APIReview />
</CSSTransition>
</StackItem>
{/* Form Action Buttons */}
<StackItem>
<Card>
<CardBody>
<Split hasGutter>
<SplitItem>
<Button type="submit" isLoading={createNsState.fetching}>
{isLastStep ? (isUpdate ? 'Update' : 'Create') : 'Next'}
</Button>
</SplitItem>
<SplitItem>
<Button variant="secondary" onClick={onPrev} isDisabled={wizardStep === 1}>
Back
</Button>
</SplitItem>
<SplitItem isFilled>
<Link to={isUpdate ? `/apis/${namespace?.slug}` : '/apis'}>
<Button variant="link">Cancel</Button>
</Link>
</SplitItem>
{isUpdate && (
<SplitItem>
<Button variant="link" isDanger onClick={setIsDeleteConfirmationOpen.on}>
Delete
</Button>
</SplitItem>
)}
</Split>
</CardBody>
</Card>
</StackItem>
</Stack>
</FormProvider>
</Form>
<Modal
variant={ModalVariant.medium}
title={`Delete ${namespace?.name} API`}
titleIconVariant="danger"
isOpen={isDeleteConfirmationOpen}
onClose={setIsDeleteConfirmationOpen.off}
actions={[
<Button
key="confirm"
variant="primary"
onClick={handleApiDelete}
isLoading={deleteNamespaceState.fetching}
>
Confirm
</Button>,
<Button key="cancel" variant="link" onClick={setIsDeleteConfirmationOpen.off}>
Cancel
</Button>,
]}
>
This action is irreversible. Are you sure you want to delete this API?
</Modal>
</PageSection>
);
}
Example #8
Source File: EnvSchemaField.tsx From one-platform with MIT License | 4 votes |
EnvSchemaField = forwardRef<HTMLInputElement, Props>(
(
{ isGraphqlAPI, isError, errorMessage, envIndex, onCopyValue, onRedoValidation, ...inputProps },
ref
): JSX.Element => {
const [isValidating, setIsValidating] = useToggle();
const handleRedoValidation = async () => {
setIsValidating.on();
await onRedoValidation();
setIsValidating.off();
};
return (
<FormGroup
fieldId={`environments.${envIndex}.schemaEndpoint`}
label={isGraphqlAPI ? 'Introspection URL' : 'API Schema URL'}
validated={isError ? 'error' : 'success'}
helperTextInvalid={errorMessage}
labelIcon={
isGraphqlAPI ? (
<Popover
headerContent={<div>What is Introsepection URL?</div>}
bodyContent={
<div>
GraphQL schema for information about what queries it supports. GraphQL allows us
to do so using the introspection system. For more information visit:{' '}
<a
href="https://graphql.org/learn/introspection/"
target="_blank"
rel="noreferrer noopener"
>
Doc
</a>
</div>
}
>
<button
type="button"
aria-label="More info for name field"
onClick={(e) => e.preventDefault()}
aria-describedby="simple-form-name-01"
className="pf-c-form__group-label-help"
>
<HelpIcon noVerticalAlign />
</button>
</Popover>
) : undefined
}
helperText={
isGraphqlAPI &&
!inputProps.value && (
<Button
variant="plain"
className="pf-u-font-size-xs pf-u-px-0 pf-u-mt-xs"
onClick={onCopyValue}
>
Click here to use API Base Path as introspection URL
</Button>
)
}
>
<Split>
<SplitItem isFilled>
<TextInput
aria-label="env link"
placeholder="Enter api schema URL"
ref={ref}
{...inputProps}
onBlur={(event) => {
handleRedoValidation();
if (inputProps?.onBlur) inputProps?.onBlur(event);
}}
/>
</SplitItem>
<SplitItem>
<Button variant="control" onClick={handleRedoValidation}>
{isValidating ? <Spinner size="sm" /> : <UndoIcon />}
</Button>
</SplitItem>
</Split>
</FormGroup>
);
}
)
Example #9
Source File: APIDescriptionPage.tsx From one-platform with MIT License | 4 votes |
APIDescriptionPage = (): JSX.Element => {
const { slug } = useParams();
const navigate = useNavigate();
const { handleDynamicCrumbs } = useBreadcrumb();
const urlParser = useURLParser();
const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0);
const [isSubscriptionOptionOpen, setIsSubscriptionOptionOpen] = useToggle();
const [isSchemaDropdownOpen, setIsSchemaDropdownOpen] = useToggle();
const [selectedSubscriptonEnv, setSelectedSubscriptionEnv] = useState<Record<
string,
boolean
> | null>(null);
const userInfo = opcBase.auth?.getUserInfo();
const [{ fetching: isSubscribing, data: subscribedNamespace }, handleSubscribeSchemaGQL] =
useSubscribeSchema();
const { isLoading: isNamespaceLoading, data: fetchedNamespace } = useGetANamespaceBySlug({
slug,
});
const namespace = subscribedNamespace?.subscribeApiSchema || fetchedNamespace?.getNamespaceBySlug;
const id = namespace?.id;
const schemas = namespace?.schemas || [];
const selectedSchema = namespace?.schemas[selectedSchemaIndex];
// effect to add breadcrumb data
useEffect(() => {
if (!isNamespaceLoading && namespace?.name && namespace?.id) {
handleDynamicCrumbs({
'api-name': { label: namespace.name, url: `/apis/${namespace?.slug}` },
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNamespaceLoading, namespace?.name, namespace?.id]);
const hasEditAccess = useMemo(() => {
const userUuid = userInfo?.rhatUUID;
return hasUserApiEditAccess(userUuid as string, namespace);
}, [namespace, userInfo?.rhatUUID]);
const onMenuClick = (schemaId: string) => {
const index = namespace?.schemas.findIndex(({ id: sid }) => sid === schemaId);
if (index !== -1) {
setSelectedSchemaIndex(index || 0);
}
setIsSchemaDropdownOpen.off();
};
const hasSubscribed = selectedSchema?.environments.some(({ isSubscribed }) => isSubscribed);
const handleSchemaSubscription = async (envIDs?: string[]) => {
const subscribedIds =
envIDs ||
(hasSubscribed ? [] : (selectedSchema?.environments || []).map(({ id: sID }) => sID));
const subscriptionConfig = {
namespaceID: id as string,
schemaID: selectedSchema?.id || '',
envIDs: subscribedIds,
email: userInfo?.email || '',
};
try {
const res = await handleSubscribeSchemaGQL({ config: subscriptionConfig });
if (res.error) {
opcBase.toast.danger({
subject: `Failed to ${hasSubscribed ? 'unsubscribe' : 'subscribe'} api`,
body: res?.error?.message,
});
} else {
const subject = `${hasSubscribed ? 'Unsubscribed' : 'Subscribed'} to ${namespace?.name}`;
const body = `You will ${
hasSubscribed ? 'not be' : 'be'
} be notified regarding updates on this API`;
opcBase.toast.info({ subject, body });
}
} catch (error) {
opcBase.toast.danger({
subject: `Failed to ${hasSubscribed ? 'unsubscribe' : 'subscribe'} api`,
});
}
};
const onInitializeSelect = () => {
if (!isSubscriptionOptionOpen) {
const alreadySubscripedEnv = (selectedSchema?.environments || []).reduce<
Record<string, boolean>
>(
(prev, { isSubscribed, id: eId }) =>
isSubscribed ? { ...prev, [eId]: true } : { ...prev },
{}
);
setSelectedSubscriptionEnv(alreadySubscripedEnv);
}
setIsSubscriptionOptionOpen.toggle();
};
const onEnvSelect = (env: string) => {
const isEnvPresent = Boolean(selectedSubscriptonEnv?.[env]);
const state = { ...selectedSubscriptonEnv };
if (isEnvPresent) {
delete state[env];
setSelectedSubscriptionEnv(state);
} else {
state[env] = true;
setSelectedSubscriptionEnv(state);
}
};
const onEnvSelectBlur = async () => {
if (selectedSubscriptonEnv) {
await handleSchemaSubscription(Object.keys(selectedSubscriptonEnv));
}
setIsSubscriptionOptionOpen.off();
setSelectedSubscriptionEnv(null);
};
if (isNamespaceLoading) {
return (
<Bullseye>
<Spinner size="xl" />
</Bullseye>
);
}
if (!namespace) {
return (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h4" size="lg">
Sorry, Couldn't find this API
</Title>
<Button variant="primary" onClick={() => navigate('../')}>
Go Back
</Button>
</EmptyState>
</Bullseye>
);
}
return (
<Stack>
<StackItem>
<PageSection isWidthLimited isCenterAligned>
<Grid hasGutter>
<GridItem span={8}>
<DetailsSection namespace={namespace} id={slug} hasEditAccess={hasEditAccess} />
</GridItem>
<GridItem span={4}>
<ApiSchemaList
schemas={namespace?.schemas}
onClick={onMenuClick}
selectedSchemaID={selectedSchema?.id}
/>
</GridItem>
</Grid>
</PageSection>
</StackItem>
<StackItem>
<PageSection
isWidthLimited
isCenterAligned
padding={{ default: 'noPadding' }}
className="pf-u-py-sm pf-u-px-md"
>
<Text component={TextVariants.small} className="pf-u-color-400">
API Schema
</Text>
</PageSection>
</StackItem>
<StackItem>
<Divider />
</StackItem>
<StackItem>
<PageSection isWidthLimited isCenterAligned className="pf-u-pb-4xl">
<Grid hasGutter>
{selectedSchema?.flags.isDeprecated && (
<Grid span={12}>
<Alert variant="danger" isInline title={`${selectedSchema.name} is deprecated`} />
</Grid>
)}
<GridItem span={8}>
<Stack
hasGutter
style={{ '--pf-l-stack--m-gutter--MarginBottom': '1.5rem' } as CSSProperties}
>
<StackItem className={styles.schemaContainer}>
<Split>
<SplitItem isFilled>
<Button
variant="link"
icon={<CaretDownIcon />}
onClick={setIsSchemaDropdownOpen.toggle}
iconPosition="right"
style={{ color: 'black' }}
className={styles.schemaDropdownTitle}
>
{selectedSchema?.name}
</Button>
</SplitItem>
<SplitItem className="pf-u-mr-lg">
<Label color={selectedSchema?.flags?.isInternal ? 'blue' : 'green'} isCompact>
{selectedSchema?.flags?.isInternal ? 'Internal API' : 'External API'}
</Label>
</SplitItem>
</Split>
<CSSTransition
in={isSchemaDropdownOpen}
timeout={200}
classNames="fade-in"
unmountOnExit
>
<Menu className={styles.schemaMenu}>
<MenuContent>
<MenuList className="pf-u-py-0">
{schemas.map((schema, index) => (
<Fragment key={schema.id}>
<MenuItem
className={css({
'menu-selected': schema.id === selectedSchema?.id,
})}
icon={
<Avatar
src={`${config.baseURL}/images/${
schema.category === 'REST'
? 'swagger-black-logo.svg'
: 'graphql-logo.svg'
}`}
alt="api-type"
size="sm"
style={{ width: '1.25rem', height: '1.25rem' }}
className="pf-u-mt-sm"
/>
}
onClick={() => onMenuClick(schema.id)}
>
<Split>
<SplitItem isFilled>{schema.name}</SplitItem>
<SplitItem>
<Label
color={schema.flags.isInternal ? 'blue' : 'green'}
isCompact
className="pf-u-ml-sm"
>
{schema.flags.isInternal ? 'Internal' : 'External'}
</Label>
</SplitItem>
</Split>
</MenuItem>
{schemas.length - 1 !== index && (
<Divider component="li" className="pf-u-my-0" />
)}
</Fragment>
))}
</MenuList>
</MenuContent>
</Menu>
</CSSTransition>
</StackItem>
<StackItem>
<ReadMore>{selectedSchema?.description || ''}</ReadMore>
</StackItem>
<StackItem>
<Split hasGutter>
<SplitItem isFilled>
<Title headingLevel="h3">Application URL</Title>
<a href={selectedSchema?.appURL} target="_blank" rel="noopener noreferrer">
<Text className="pf-u-color-400">
{urlParser(selectedSchema?.appURL || '')}
</Text>
</a>
</SplitItem>
<SplitItem isFilled>
<Title headingLevel="h3">Documentation URL</Title>
<a href={selectedSchema?.docURL} target="_blank" rel="noopener noreferrer">
<Text className="pf-u-color-400">
{urlParser(selectedSchema?.docURL || '')}
</Text>
</a>
</SplitItem>
</Split>
</StackItem>
<StackItem className="pf-u-mt-md">
<ApiEnvironmentSection
environments={selectedSchema?.environments}
category={selectedSchema?.category}
/>
</StackItem>
</Stack>
</GridItem>
<GridItem span={1} />
<GridItem span={3}>
<Stack hasGutter>
<StackItem className={styles.subscriptionContainer}>
<Split>
<SplitItem isFilled>
<Button
icon={<BellIcon />}
variant={hasSubscribed ? 'primary' : 'secondary'}
iconPosition="right"
isBlock
isLoading={isSubscribing}
className={css(hasSubscribed ? styles.subscriptionDropdownBtn : null)}
onClick={() => handleSchemaSubscription()}
>
{hasSubscribed ? 'Subscribed' : 'Subscribe'}
</Button>
</SplitItem>
<CSSTransition
in={hasSubscribed}
timeout={200}
classNames="fade-in"
unmountOnExit
>
<SplitItem>
<Button
icon={<CaretDownIcon />}
onClick={onInitializeSelect}
className={css('ignore-blur', styles.subscriptionDropdownArrow)}
/>
</SplitItem>
</CSSTransition>
</Split>
<CSSTransition
in={isSubscriptionOptionOpen}
timeout={200}
classNames="fade-in"
unmountOnExit
>
<Menu
className={styles.subscriptionMenu}
onBlur={(e) => {
if (!e.relatedTarget?.className?.includes('ignore-blur')) {
onEnvSelectBlur();
}
}}
>
<MenuContent>
<MenuList className="pf-u-py-0">
{selectedSchema?.environments.map(({ name, isSubscribed, id: envId }) => (
<MenuItem
className="uppercase ignore-blur"
isSelected={
selectedSubscriptonEnv
? selectedSubscriptonEnv[envId]
: isSubscribed
}
key={`subscription-${envId}`}
itemId={envId}
onClick={() => onEnvSelect(envId)}
>
{name}
</MenuItem>
))}
</MenuList>
</MenuContent>
</Menu>
</CSSTransition>
</StackItem>
<StackItem>
<ApiTypeCard category={selectedSchema?.category} />
</StackItem>
</Stack>
</GridItem>
</Grid>
</PageSection>
</StackItem>
</Stack>
);
}
Example #10
Source File: APIListPage.tsx From one-platform with MIT License | 4 votes |
APIListPage = (): JSX.Element => {
const navigate = useNavigate();
// query param strings
const query = useQueryParams();
const mid = query.get('mid');
const defaultSearch = query.get('search');
// filters, search, sorting
const [isSortSelectOpen, setSortSelect] = useToggle();
const [sortOption, setSortOption] = useState(SortBy.RECENTLY_ADDED);
const [filters, setFilters] = useState<{ type: null | ApiCategory; search: string }>({
type: null,
search: defaultSearch || '',
});
const { pagination, onPerPageSelect, onSetPage, onResetPagination } = usePagination({
page: 1,
perPage: 20,
});
const debouncedSearch = useDebounce(filters.search);
// graphql query hooks
const { isLoading: isApiListLoading, data: namespaceList } = useGetNamespaceList({
limit: pagination.perPage,
offset: (pagination.page - 1) * pagination.perPage,
apiCategory: filters.type,
search: debouncedSearch,
sortBy: sortOption === SortBy.RECENTLY_ADDED ? 'CREATED_ON' : 'UPDATED_ON',
mid,
});
const { isLoading: isNamespaceStatLoading, data: namespaceStats } = useGetNamespaceStats({
search: debouncedSearch,
mid,
});
const handleApiOwnersRender = useCallback((owners: ApiOwnerType[]) => {
return owners.map((owner) =>
owner.group === ApiEmailGroup.USER ? owner.user.cn : owner.email
);
}, []);
const onStatCardClick = (cardType: 'total' | 'rest' | 'graphql') => {
onResetPagination();
if (cardType === 'total') {
setFilters((state) => ({ ...state, type: null }));
} else {
setFilters((state) => ({ ...state, type: cardType.toUpperCase() as ApiCategory }));
}
};
const onSearch = (search: string) => {
setFilters((state) => ({ ...state, search }));
};
const onSortSelect = (
event: React.MouseEvent | React.ChangeEvent,
value: string | SelectOptionObject,
isPlaceholder?: boolean
) => {
if (isPlaceholder) setSortOption(SortBy.RECENTLY_ADDED);
else setSortOption(value as SortBy);
setSortSelect.off(); // close the select
};
const onCardClick = (id: string) => {
navigate(id);
};
const namespaceCount = namespaceStats?.getApiCategoryCount;
const namespaces = namespaceList?.listNamespaces?.data;
const isNamespaceEmpty = !isApiListLoading && namespaces?.length === 0;
return (
<>
<Header />
<Divider />
<PageSection variant="light" isWidthLimited className="pf-m-align-center">
<Grid hasGutter>
<Grid hasGutter span={12}>
{stats.map(({ key, type, image }) => (
<GridItem
key={`api-select-${type}`}
span={4}
className={styles['api-list--stat-card']}
type={key}
>
<StatCard
value={namespaceCount?.[key]}
category={type}
isLoading={isNamespaceStatLoading}
onClick={callbackify(onStatCardClick, key)}
isSelected={filters.type ? filters.type.toLowerCase() === key : key === 'total'}
>
<img
src={`${config.baseURL}/images/${image}`}
alt={`api-select-${type}`}
style={{ height: '48px' }}
/>
</StatCard>
</GridItem>
))}
</Grid>
<GridItem className="pf-u-my-md">
<Split hasGutter className={styles['api-list--table-filter--container']}>
<SplitItem isFilled>
<Link to={ApiCatalogLinks.AddNewApiPage}>
<Button>Add API</Button>
</Link>
</SplitItem>
<SplitItem className="pf-u-w-33">
<Form>
<FormGroup fieldId="search">
<TextInput
aria-label="Search API"
placeholder="Search for APIs"
type="search"
iconVariant="search"
value={filters.search}
onChange={onSearch}
/>
</FormGroup>
</Form>
</SplitItem>
<SplitItem style={{ width: '180px' }}>
<Select
isOpen={isSortSelectOpen}
onToggle={setSortSelect.toggle}
selections={sortOption}
onSelect={onSortSelect}
>
{[
<SelectOption key="select-sort-placeholder" value="Sort by" isDisabled />,
<SelectOption
key={`select-sort:${SortBy.RECENTLY_ADDED}`}
value={SortBy.RECENTLY_ADDED}
/>,
<SelectOption
key={`select-sort:${SortBy.RECENTLY_MODIFIED}`}
value={SortBy.RECENTLY_MODIFIED}
/>,
]}
</Select>
</SplitItem>
</Split>
</GridItem>
{isApiListLoading ? (
<Bullseye className="pf-u-mt-lg">
<Spinner size="xl" />
</Bullseye>
) : (
namespaces?.map(({ id, name, updatedOn, owners, schemas, slug }) => (
<GridItem
span={12}
key={id}
className="catalog-nav-link"
onClick={callbackify(onCardClick, slug)}
>
<ApiDetailsCard
title={name}
owners={handleApiOwnersRender(owners)}
updatedAt={updatedOn}
schemas={schemas.map(({ name: schemaName, category }) => ({
name: schemaName,
type: category,
}))}
/>
</GridItem>
))
)}
{isNamespaceEmpty && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h4" size="lg">
No API found
</Title>
<EmptyStateBody>Add an API to fill this gap</EmptyStateBody>
</EmptyState>
)}
</Grid>
</PageSection>
<PageSection variant="light" isWidthLimited className="pf-m-align-center pf-u-pb-2xl">
<Pagination
itemCount={namespaceList?.listNamespaces?.count || 0}
widgetId="pagination-options-menu-bottom"
perPage={pagination.perPage}
page={pagination.page}
onSetPage={(_, page) => onSetPage(page)}
onPerPageSelect={(_, perPage) => onPerPageSelect(perPage)}
isCompact
/>
</PageSection>
</>
);
}
Example #11
Source File: GQLPlaygroundToolboxPage.tsx From one-platform with MIT License | 4 votes |
GQLPlaygroundToolboxPage = (): JSX.Element => {
const { envSlug } = useParams();
const navigate = useNavigate();
const { pathname } = useLocation();
const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
const [isDecodingFile, setIsDecodingFile] = useToggle();
const schema = schemaData?.fetchAPISchema?.schema;
const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
const file = schemaData?.fetchAPISchema?.file;
useRegisterRecentVisit({
isLoading: isLoading || isDecodingFile,
log: useMemo(
() => ({
title: schema?.name || '',
tool: 'playground',
url: pathname,
id: namespaceSlug as string,
envSlug: envSlug as string,
}),
[pathname, namespaceSlug, schema?.name, envSlug]
),
onRemoveId: namespaceSlug,
});
const env = useMemo(() => {
if (schema) {
return schema.environments.find(({ slug }) => slug === envSlug);
}
return { apiBasePath: '' };
}, [schema, envSlug]);
const schemaFile = useMemo(() => {
if (file) {
try {
setIsDecodingFile.on();
const data = JSON.parse(window.atob(file));
return data.data as object;
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to parse file!!',
});
} finally {
setIsDecodingFile.off();
}
}
return '';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [file]);
if (isLoading || isDecodingFile) {
return (
<Bullseye>
<Spinner size="xl" />
</Bullseye>
);
}
if (!file) {
return (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={CubeIcon} />
<Title headingLevel="h4" size="lg">
Sorry, Couldn't find this API
</Title>
<Button variant="primary" onClick={() => navigate('/apis')}>
Go Back
</Button>
</EmptyState>
</Bullseye>
);
}
return (
<Provider store={store}>
<Playground
endpoint={env?.apiBasePath}
schema={schemaFile}
settings={{
'editor.theme': 'light',
'schema.enablePolling': false,
}}
/>
</Provider>
);
}
Example #12
Source File: RedocToolboxPage.tsx From one-platform with MIT License | 4 votes |
RedocToolboxPage = (): JSX.Element => {
const { envSlug } = useParams();
const navigate = useNavigate();
const { pathname } = useLocation();
const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
const [isDecodingFile, setIsDecodingFile] = useToggle();
const redocContainer = useRef<HTMLDivElement>(null);
const schema = schemaData?.fetchAPISchema?.schema;
const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
const file = schemaData?.fetchAPISchema?.file;
useEffect(() => {
loadThirdPartyScript(
'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js',
'redoc-script',
() => {
window.process = { ...(window.process || {}), cwd: () => '' };
},
() =>
window.OpNotification.danger({
subject: 'Failed to load redoc',
})
);
}, []);
useEffect(() => {
if (file && typeof Redoc !== 'undefined') {
try {
setIsDecodingFile.on();
const data = yaml.load(window.atob(file));
Redoc.init(data, {}, document.getElementById('redoc-container'));
} catch (error) {
window.OpNotification.danger({
subject: 'Failed to parse file!!',
});
} finally {
setIsDecodingFile.off();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [file, typeof Redoc]);
useRegisterRecentVisit({
isLoading: isLoading || isDecodingFile,
log: useMemo(
() => ({
title: schema?.name || '',
tool: 'redoc',
url: pathname,
id: namespaceSlug as string,
envSlug: envSlug as string,
}),
[pathname, namespaceSlug, schema?.name, envSlug]
),
onRemoveId: namespaceSlug,
});
if (isLoading || isDecodingFile) {
return (
<Bullseye>
<Spinner size="xl" />
</Bullseye>
);
}
if (!file) {
return (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={CubeIcon} />
<Title headingLevel="h4" size="lg">
Sorry, Couldn't find this API
</Title>
<Button variant="primary" onClick={() => navigate('/apis')}>
Go Back
</Button>
</EmptyState>
</Bullseye>
);
}
return <div id="redoc-container" ref={redocContainer} />;
}
Example #13
Source File: HomePage.tsx From one-platform with MIT License | 4 votes |
HomePage = (): JSX.Element => {
const query = useQueryParams();
// modal state hooks
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
{ name: 'detailView', isOpen: Boolean(query.get('id')) },
{ name: 'appList', isOpen: false },
]);
// hooks for api filtering, pagination
const [filters, setFilters] = useState<FeedbackFilters>({
selectedApps: null,
status: null,
category: null,
search: '',
});
const { pagination, onPerPageSelect, onSetPage } = usePagination();
const [isMyFeedback, setIsMyFeedback] = useToggle();
const [isExporting, setIsExporting] = useToggle();
const debouncedSearch = useDebounce(filters.search, 500);
// convert the filters to graphl variable format for api calls
const queryFilters = useMemo<GetFeedbackListQueryVariables>(() => {
const appIds = Object.keys(filters.selectedApps || {});
const userInfo = opcBase.auth?.getUserInfo();
return {
...filters,
search: debouncedSearch,
appId: appIds.length !== 0 ? appIds : null,
createdBy: isMyFeedback ? userInfo?.rhatUUID : null,
limit: pagination.perPage,
offset: (pagination.page - 1) * pagination.perPage,
};
}, [filters, debouncedSearch, isMyFeedback, pagination]);
// Get app list
const [{ data: appList, fetching: isAppListLoading }] = useQuery<GetAppsQuery>({
query: GetApps,
});
/**
* This query will be executed only when there is ?id=<feedback_id>
* To fetch a particular feedback
*/
const [{ data: feedbackById, fetching: isFeedbackByIdQueryLoading }] = useQuery<
GetFeedbackById,
{ id: string }
>({
query: GetFeedback,
variables: { id: query?.get('id') as string },
pause: !query?.get('id'),
});
// Get feedback list
const [{ data: fetchedFeedback, fetching: isFeedbackListLoading }] = useQuery<
GetFeedbackListQuery,
GetFeedbackListQueryVariables
>({
query: GetFeedbackList,
variables: queryFilters,
});
const feedbacks = fetchedFeedback?.listFeedbacks;
const selectedFeedback = (popUp?.detailView?.data as Feedback) || feedbackById?.getFeedbackById;
// To keep all selectedApps sorted to top
const selectedApps = useMemo(() => {
if (!appList?.apps) return [];
const apps = [...appList.apps];
return apps
.sort(({ id }) => (filters.selectedApps?.[id] ? -1 : 1)) // sort it with selected apps at top
.slice(0, Math.max(Object.keys(filters.selectedApps || {}).length, 5)); // either show 5 apps or all the selected ones
}, [appList?.apps, filters.selectedApps]);
/**
* To show the title of the ticket created
* Computes it based on JIRA, Github, Gitlab
*/
const formatePopupTitle = useCallback((type: string, url: string) => {
if (!type || !url) {
return '';
}
const splittedUrl = url.split('/');
const ticketName = splittedUrl[splittedUrl.length - 1];
if (type.toLowerCase() === 'jira') {
return ticketName;
}
return `${type} ${ticketName}`;
}, []);
const handleFeedbackFilterChange = useCallback(
<T extends unknown>(field: keyof FeedbackFilters, value: T) => {
setFilters((state) => ({ ...state, [field]: value }));
},
[]
);
const handleFeedbackFilterAppIdChange = useCallback(
(app: App) => {
const appsSelected = { ...filters.selectedApps };
if (appsSelected?.[app.id]) {
delete appsSelected[app.id];
} else {
appsSelected[app.id] = app;
}
setFilters((state) => ({ ...state, selectedApps: appsSelected }));
},
[filters.selectedApps]
);
const handleFeedbackFilterClear = useCallback((field: keyof FeedbackFilters) => {
setFilters((state) => ({ ...state, [field]: null }));
}, []);
const onExportToCSV = () => {
if (!feedbacks?.data) return;
setIsExporting.on();
/**
* Format the feedback list json response for the csv
* Set the headers and pick required fields only
*/
const formatedFeedbacks = feedbacks?.data.map((feedback: Record<string, unknown>) => {
const formatedFeedback: Record<string, unknown> = {};
EXPORT_FEEDBACK_CSV.forEach(({ title, field }) => {
const value = field.split('.').reduce((obj, i) => obj[i] as any, feedback);
formatedFeedback[title] = value;
});
return formatedFeedback;
});
jsonexport(formatedFeedbacks, (err, csv) => {
setIsExporting.off();
if (err) {
opcBase.toast.danger({ subject: 'Failed to export csv' });
} else {
// export to csv
let csvContent = 'data:text/csv;charset=utf-8,';
csvContent += csv;
const encodedCsv = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedCsv);
link.setAttribute('download', 'Feedback.csv');
link.click();
opcBase.toast.success({
subject: 'Export sucessfully completed',
});
}
});
};
return (
<>
<PageSection isWidthLimited variant="light" className=" pf-m-align-center">
<Grid hasGutter style={{ '--pf-l-grid--m-gutter--GridGap': '2rem' } as CSSProperties}>
<GridItem span={3}>
<SearchInput
type="search"
id="search-feedback"
placeholder="Search via name"
value={filters.search || ''}
onChange={(value) => handleFeedbackFilterChange('search', value)}
/>
</GridItem>
<GridItem span={9}>
<Split>
<SplitItem isFilled>
<Button variant="primary" onClick={setIsMyFeedback.toggle}>
{`${isMyFeedback ? 'All' : 'My'} Feedback`}
</Button>
</SplitItem>
<SplitItem>
<Button
icon={<UploadIcon />}
variant="secondary"
onClick={onExportToCSV}
isLoading={isExporting}
isDisabled={!feedbacks?.count}
>
Export
</Button>
</SplitItem>
</Split>
</GridItem>
<GridItem span={3}>
<form>
<Stack hasGutter>
<StackItem>
<Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
<StackItem>
<FilterTitle
title="Applications"
onClear={() => handleFeedbackFilterClear('selectedApps')}
isClearable={Boolean(filters.selectedApps)}
/>
</StackItem>
{isAppListLoading ? (
<Bullseye>
<Spinner size="lg" label="Loading..." />
</Bullseye>
) : (
<>
{selectedApps.map((app) => (
<StackItem key={app.id}>
<Checkbox
id={app.id}
label={app.name}
className="capitalize"
isChecked={Boolean(filters.selectedApps?.[app.id])}
onChange={() => handleFeedbackFilterAppIdChange(app)}
/>
</StackItem>
))}
{(appList?.apps || [])?.length > 5 && (
<StackItem>
<Button
variant="link"
icon={<PlusIcon />}
onClick={() => handlePopUpOpen('appList')}
>
Expand to see more apps
</Button>
</StackItem>
)}
</>
)}
</Stack>
</StackItem>
<StackItem>
<Divider />
</StackItem>
<StackItem>
<Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
<StackItem>
<FilterTitle
title="Type"
onClear={() => handleFeedbackFilterClear('category')}
isClearable={Boolean(filters.category)}
/>
</StackItem>
<StackItem>
<Radio
id="feedback-type-1"
label="Bug"
name="type"
isChecked={filters?.category === FeedbackCategoryAPI.BUG}
onChange={() =>
handleFeedbackFilterChange('category', FeedbackCategoryAPI.BUG)
}
/>
</StackItem>
<StackItem>
<Radio
id="feedback-type-2"
label="Feedback"
name="type"
isChecked={filters?.category === FeedbackCategoryAPI.FEEDBACK}
onChange={() =>
handleFeedbackFilterChange('category', FeedbackCategoryAPI.FEEDBACK)
}
/>
</StackItem>
</Stack>
</StackItem>
<StackItem>
<Divider />
</StackItem>
<StackItem>
<Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
<StackItem>
<FilterTitle
title="Status"
onClear={() => handleFeedbackFilterClear('status')}
isClearable={Boolean(filters.status)}
/>
</StackItem>
<StackItem>
<Radio
id="feedback-status-1"
label="Open"
name="status"
isChecked={filters?.status === FeedbackStatusAPI.OPEN}
onChange={() =>
handleFeedbackFilterChange('status', FeedbackStatusAPI.OPEN)
}
/>
</StackItem>
<StackItem>
<Radio
id="feedback-status-2"
label="Closed"
name="status"
isChecked={filters?.status === FeedbackStatusAPI.CLOSED}
onChange={() =>
handleFeedbackFilterChange('status', FeedbackStatusAPI.CLOSED)
}
/>
</StackItem>
</Stack>
</StackItem>
<StackItem>
<Divider />
</StackItem>
</Stack>
</form>
</GridItem>
<GridItem span={9}>
<Stack hasGutter>
{isFeedbackListLoading || feedbacks?.count === 0 ? (
<EmptyState>
<EmptyStateIcon
variant={isFeedbackListLoading ? 'container' : 'icon'}
component={isFeedbackListLoading ? Spinner : undefined}
icon={CubesIcon}
/>
<Title size="lg" headingLevel="h4">
{isFeedbackListLoading ? 'Loading' : 'No feedback found!!'}
</Title>
</EmptyState>
) : (
feedbacks?.data?.map((feedback) => (
<StackItem key={feedback.id}>
<FeedbackCard
title={(feedback.createdBy as FeedbackUserProfileAPI)?.cn}
createdOn={feedback.createdOn}
description={feedback.summary}
experience={feedback.experience}
error={feedback.error}
module={feedback.module}
category={feedback.category}
state={feedback.state}
onClick={() => handlePopUpOpen('detailView', feedback)}
/>
</StackItem>
))
)}
<StackItem>
<Pagination
itemCount={feedbacks?.count}
isCompact
perPage={pagination.perPage}
page={pagination.page}
onSetPage={(_evt, newPage) => onSetPage(newPage)}
widgetId="feedback-pagination"
onPerPageSelect={(_evt, perPage) => onPerPageSelect(perPage)}
/>
</StackItem>
</Stack>
</GridItem>
</Grid>
</PageSection>
<Modal
variant={ModalVariant.small}
title={formatePopupTitle(selectedFeedback?.source, selectedFeedback?.ticketUrl)}
isOpen={popUp.detailView.isOpen}
onClose={() => handlePopUpClose('detailView')}
footer={
<Split hasGutter style={{ width: '100%' }}>
<SplitItem isFilled>
<a href={selectedFeedback?.ticketUrl} target="_blank" rel="noopener noreferrer">
<Button isSmall key="more" variant="danger">
See {selectedFeedback?.source} Issue
</Button>
</a>
</SplitItem>
<SplitItem>
<Button
key="close"
variant="tertiary"
isSmall
onClick={() => handlePopUpClose('detailView')}
>
Close
</Button>
</SplitItem>
</Split>
}
>
<FeedbackDetailCard feedback={selectedFeedback} isLoading={isFeedbackByIdQueryLoading} />
</Modal>
<Modal
variant={ModalVariant.medium}
title="All Applications"
isOpen={popUp.appList.isOpen}
onClose={() => handlePopUpClose('appList')}
>
<AppListCard
apps={appList?.apps}
filteredApps={filters.selectedApps}
onSubmit={(apps) => {
setFilters((state) => ({ ...state, selectedApps: apps }));
handlePopUpClose('appList');
}}
/>
</Modal>
</>
);
}