1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-27 00:19:39 +01:00

Sticky batch actions bar (#3366)

This commit is contained in:
Tymoteusz Czech 2023-03-22 13:15:53 +01:00 committed by GitHub
parent e03307e286
commit 5585a9bed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 268 additions and 240 deletions

View File

@ -1,6 +1,6 @@
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { Undo } from '@mui/icons-material'; import { Delete, Undo } from '@mui/icons-material';
import { import {
DELETE_FEATURE, DELETE_FEATURE,
UPDATE_FEATURE, UPDATE_FEATURE,
@ -69,7 +69,7 @@ export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
{({ hasAccess }) => ( {({ hasAccess }) => (
<Button <Button
disabled={!hasAccess} disabled={!hasAccess}
startIcon={<Undo />} startIcon={<Delete />}
variant="outlined" variant="outlined"
size="small" size="small"
onClick={onDelete} onClick={onDelete}

View File

@ -294,64 +294,67 @@ export const ArchiveTable = ({
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps }, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<PageContent <>
isLoading={loading} <PageContent
header={ isLoading={loading}
<PageHeader header={
titleElement={`${title} (${ <PageHeader
rows.length < data.length titleElement={`${title} (${
? `${rows.length} of ${data.length}` rows.length < data.length
: data.length ? `${rows.length} of ${data.length}`
})`} : data.length
actions={ })`}
<Search actions={
initialValue={searchValue} <Search
onChange={setSearchValue} initialValue={searchValue}
hasFilters onChange={setSearchValue}
getSearchContext={getSearchContext} hasFilters
/> getSearchContext={getSearchContext}
} />
/>
}
>
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
prepareRow={prepareRow}
/>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={() => (
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
None of the feature toggles were archived yet.
</TablePlaceholder>
} }
/> />
)} }
/> >
<ArchivedFeatureDeleteConfirm <SearchHighlightProvider value={getSearchText(searchValue)}>
deletedFeatures={[deletedFeature?.name!]} <VirtualizedTable
projectId={projectId!} rows={rows}
open={deleteModalOpen} headerGroups={headerGroups}
setOpen={setDeleteModalOpen} prepareRow={prepareRow}
refetch={refetch} />
/> </SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={() => (
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
None of the feature toggles were archived
yet.
</TablePlaceholder>
}
/>
)}
/>
<ArchivedFeatureDeleteConfirm
deletedFeatures={[deletedFeature?.name!]}
projectId={projectId!}
open={deleteModalOpen}
setOpen={setDeleteModalOpen}
refetch={refetch}
/>
</PageContent>
<ConditionallyRender <ConditionallyRender
condition={Boolean(projectId)} condition={Boolean(projectId)}
show={ show={
<BatchSelectionActionsBar <BatchSelectionActionsBar
selectedIds={Object.keys(selectedRowIds)} count={Object.keys(selectedRowIds).length}
> >
<ArchiveBatchActions <ArchiveBatchActions
selectedIds={Object.keys(selectedRowIds)} selectedIds={Object.keys(selectedRowIds)}
@ -360,6 +363,6 @@ export const ArchiveTable = ({
</BatchSelectionActionsBar> </BatchSelectionActionsBar>
} }
/> />
</PageContent> </>
); );
}; };

View File

@ -2,14 +2,21 @@ import { FC } from 'react';
import { Box, Paper, styled, Typography } from '@mui/material'; import { Box, Paper, styled, Typography } from '@mui/material';
interface IBatchSelectionActionsBarProps { interface IBatchSelectionActionsBarProps {
selectedIds: string[]; count: number;
} }
const StyledContainer = styled(Box)(() => ({ const StyledStickyContainer = styled('div')(() => ({
position: 'sticky',
marginTop: 'auto',
bottom: 0,
}));
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
width: '100%', width: '100%',
flexWrap: 'wrap', flexWrap: 'wrap',
paddingBottom: theme.spacing(2),
})); }));
const StyledBar = styled(Paper)(({ theme }) => ({ const StyledBar = styled(Paper)(({ theme }) => ({
@ -40,22 +47,24 @@ const StyledText = styled(Typography)(({ theme }) => ({
})); }));
export const BatchSelectionActionsBar: FC<IBatchSelectionActionsBarProps> = ({ export const BatchSelectionActionsBar: FC<IBatchSelectionActionsBarProps> = ({
selectedIds, count,
children, children,
}) => { }) => {
if (selectedIds.length === 0) { if (count === 0) {
return null; return null;
} }
return ( return (
<StyledContainer> <StyledStickyContainer>
<StyledBar elevation={4}> <StyledContainer>
<StyledText> <StyledBar elevation={4}>
<StyledCount>{selectedIds.length}</StyledCount> <StyledText>
&ensp;selected <StyledCount>{count}</StyledCount>
</StyledText> &ensp;selected
{children} </StyledText>
</StyledBar> {children}
</StyledContainer> </StyledBar>
</StyledContainer>
</StyledStickyContainer>
); );
}; };

View File

@ -91,7 +91,6 @@ export const InstanceStatus: FC = ({ children }) => {
useInstanceStatus(); useInstanceStatus();
const { extendTrial } = useInstanceStatusApi(); const { extendTrial } = useInstanceStatusApi();
const { setToastApiError } = useToast(); const { setToastApiError } = useToast();
const theme = useTheme();
const onExtendTrial = async () => { const onExtendTrial = async () => {
try { try {
@ -103,12 +102,7 @@ export const InstanceStatus: FC = ({ children }) => {
}; };
return ( return (
<div <>
style={{
height: '100%',
backgroundColor: theme.palette.background.paper,
}}
>
<ConditionallyRender <ConditionallyRender
condition={isBilling && Boolean(instanceStatus)} condition={isBilling && Boolean(instanceStatus)}
show={() => ( show={() => (
@ -124,7 +118,7 @@ export const InstanceStatus: FC = ({ children }) => {
)} )}
/> />
{children} {children}
</div> </>
); );
}; };

View File

@ -21,12 +21,15 @@ interface IMainLayoutProps {
const MainLayoutContainer = styled(Grid)(() => ({ const MainLayoutContainer = styled(Grid)(() => ({
height: '100%', height: '100%',
justifyContent: 'space-between', justifyContent: 'space-between',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
position: 'relative',
})); }));
const MainLayoutContentWrapper = styled('main')(({ theme }) => ({ const MainLayoutContentWrapper = styled('main')(({ theme }) => ({
margin: theme.spacing(0, 'auto'), margin: theme.spacing(0, 'auto'),
overflow: 'auto', // prevent margin collapsing flexGrow: 1,
flex: 1,
width: '100%', width: '100%',
backgroundColor: theme.palette.background.application, backgroundColor: theme.palette.background.application,
position: 'relative', position: 'relative',

View File

@ -554,174 +554,187 @@ export const ProjectFeatureToggles = ({
]); ]);
return ( return (
<PageContent <>
isLoading={loading} <PageContent
className={styles.container} isLoading={loading}
header={ className={styles.container}
<PageHeader header={
titleElement={`Feature toggles (${rows.length})`} <PageHeader
actions={ titleElement={`Feature toggles (${rows.length})`}
<> actions={
<ConditionallyRender <>
condition={!isSmallScreen} <ConditionallyRender
show={ condition={!isSmallScreen}
<Search show={
initialValue={searchValue} <Search
onChange={setSearchValue} initialValue={searchValue}
hasFilters onChange={setSearchValue}
getSearchContext={getSearchContext} hasFilters
/> getSearchContext={getSearchContext}
} />
/> }
<ColumnsMenu />
allColumns={allColumns} <ColumnsMenu
staticColumns={staticColumns} allColumns={allColumns}
dividerAfter={['createdAt']} staticColumns={staticColumns}
dividerBefore={['Actions']} dividerAfter={['createdAt']}
isCustomized={Boolean(storedParams.columns)} dividerBefore={['Actions']}
setHiddenColumns={setHiddenColumns} isCustomized={Boolean(storedParams.columns)}
/> setHiddenColumns={setHiddenColumns}
<PageHeader.Divider sx={{ marginLeft: 0 }} /> />
<ConditionallyRender <PageHeader.Divider sx={{ marginLeft: 0 }} />
condition={Boolean( <ConditionallyRender
uiConfig?.flags?.featuresExportImport condition={Boolean(
)} uiConfig?.flags?.featuresExportImport
show={ )}
<Tooltip show={
title="Export toggles visible in the table below" <Tooltip
arrow title="Export toggles visible in the table below"
> arrow
<IconButton
onClick={() =>
setShowExportDialog(true)
}
sx={theme => ({
marginRight: theme.spacing(2),
})}
> >
<FileDownload /> <IconButton
</IconButton> onClick={() =>
</Tooltip> setShowExportDialog(true)
} }
/> sx={theme => ({
<StyledResponsiveButton marginRight:
onClick={() => theme.spacing(2),
navigate(getCreateTogglePath(projectId)) })}
} >
maxWidth="960px" <FileDownload />
Icon={Add} </IconButton>
projectId={projectId} </Tooltip>
permission={CREATE_FEATURE} }
data-testid="NAVIGATE_TO_CREATE_FEATURE" />
> <StyledResponsiveButton
New feature toggle onClick={() =>
</StyledResponsiveButton> navigate(getCreateTogglePath(projectId))
</> }
maxWidth="960px"
Icon={Add}
projectId={projectId}
permission={CREATE_FEATURE}
data-testid="NAVIGATE_TO_CREATE_FEATURE"
>
New feature toggle
</StyledResponsiveButton>
</>
}
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
}
/>
</PageHeader>
}
>
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
prepareRow={prepareRow}
/>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No feature toggles available. Get started by
adding a new feature toggle.
</TablePlaceholder>
}
/>
} }
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
}
/>
</PageHeader>
}
>
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
prepareRow={prepareRow}
/> />
</SearchHighlightProvider> <EnvironmentStrategyDialog
<ConditionallyRender onClose={() =>
condition={rows.length === 0} setStrategiesDialogState(prev => ({
show={ ...prev,
<ConditionallyRender open: false,
condition={searchValue?.length > 0} }))
show={ }
<TablePlaceholder> projectId={projectId}
No feature toggles found matching &ldquo; {...strategiesDialogState}
{searchValue} />
&rdquo; <FeatureStaleDialog
</TablePlaceholder> isStale={featureStaleDialogState.stale === true}
} isOpen={Boolean(featureStaleDialogState.featureId)}
elseShow={ onClose={() => {
<TablePlaceholder> setFeatureStaleDialogState({});
No feature toggles available. Get started by refetch();
adding a new feature toggle. }}
</TablePlaceholder> featureId={featureStaleDialogState.featureId || ''}
} projectId={projectId}
/> />
} <FeatureArchiveDialog
/> isOpen={Boolean(featureArchiveState)}
<EnvironmentStrategyDialog onConfirm={() => {
onClose={() => refetch();
setStrategiesDialogState(prev => ({ ...prev, open: false })) }}
} onClose={() => {
projectId={projectId} setFeatureArchiveState(undefined);
{...strategiesDialogState} }}
/> featureIds={[featureArchiveState || '']}
<FeatureStaleDialog projectId={projectId}
isStale={featureStaleDialogState.stale === true} />{' '}
isOpen={Boolean(featureStaleDialogState.featureId)} <ChangeRequestDialogue
onClose={() => { isOpen={changeRequestDialogDetails.isOpen}
setFeatureStaleDialogState({}); onClose={onChangeRequestToggleClose}
refetch(); environment={changeRequestDialogDetails?.environment}
}} onConfirm={onChangeRequestToggleConfirm}
featureId={featureStaleDialogState.featureId || ''} messageComponent={
projectId={projectId} <UpdateEnabledMessage
/> featureName={
<FeatureArchiveDialog changeRequestDialogDetails.featureName!
isOpen={Boolean(featureArchiveState)} }
onConfirm={() => { enabled={changeRequestDialogDetails.enabled!}
refetch(); environment={
}} changeRequestDialogDetails?.environment!
onClose={() => { }
setFeatureArchiveState(undefined); />
}} }
featureIds={[featureArchiveState || '']} />
projectId={projectId} <ConditionallyRender
/>{' '} condition={
<ChangeRequestDialogue Boolean(uiConfig?.flags?.featuresExportImport) &&
isOpen={changeRequestDialogDetails.isOpen} !loading
onClose={onChangeRequestToggleClose} }
environment={changeRequestDialogDetails?.environment} show={
onConfirm={onChangeRequestToggleConfirm} <ExportDialog
messageComponent={ showExportDialog={showExportDialog}
<UpdateEnabledMessage data={data}
featureName={changeRequestDialogDetails.featureName!} onClose={() => setShowExportDialog(false)}
enabled={changeRequestDialogDetails.enabled!} environments={environments}
environment={changeRequestDialogDetails?.environment!} />
/> }
} />
/> </PageContent>
<ConditionallyRender <BatchSelectionActionsBar
condition={ count={Object.keys(selectedRowIds).length}
Boolean(uiConfig?.flags?.featuresExportImport) && !loading >
}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
environments={environments}
/>
}
/>
<BatchSelectionActionsBar selectedIds={Object.keys(selectedRowIds)}>
<ProjectFeaturesBatchActions <ProjectFeaturesBatchActions
selectedIds={Object.keys(selectedRowIds)} selectedIds={Object.keys(selectedRowIds)}
data={features} data={features}
projectId={projectId} projectId={projectId}
/> />
</BatchSelectionActionsBar> </BatchSelectionActionsBar>
</PageContent> </>
); );
}; };

View File

@ -9,10 +9,13 @@ html {
} }
body { body {
height: 100%; min-height: 100%;
font-family: 'Sen', sans-serif; font-family: 'Sen', sans-serif;
font-size: 16px; font-size: 16px;
font-variant-ligatures: none; font-variant-ligatures: none;
padding: 0;
display: flex;
flex-direction: column;
} }
button { button {
@ -139,7 +142,10 @@ a:hover {
} }
#app { #app {
height: 100%; flex-grow: 1;
min-height: 100%;
display: flex;
flex-direction: column;
} }
.MuiCardHeader-title { .MuiCardHeader-title {