mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Sticky batch actions bar (#3366)
This commit is contained in:
parent
e03307e286
commit
5585a9bed0
@ -1,6 +1,6 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { Undo } from '@mui/icons-material';
|
||||
import { Delete, Undo } from '@mui/icons-material';
|
||||
import {
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
@ -69,7 +69,7 @@ export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
|
||||
{({ hasAccess }) => (
|
||||
<Button
|
||||
disabled={!hasAccess}
|
||||
startIcon={<Undo />}
|
||||
startIcon={<Delete />}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={onDelete}
|
||||
|
@ -294,64 +294,67 @@ export const ArchiveTable = ({
|
||||
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={`${title} (${
|
||||
rows.length < data.length
|
||||
? `${rows.length} of ${data.length}`
|
||||
: data.length
|
||||
})`}
|
||||
actions={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
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 “
|
||||
{searchValue}”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
None of the feature toggles were archived yet.
|
||||
</TablePlaceholder>
|
||||
<>
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={`${title} (${
|
||||
rows.length < data.length
|
||||
? `${rows.length} of ${data.length}`
|
||||
: data.length
|
||||
})`}
|
||||
actions={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ArchivedFeatureDeleteConfirm
|
||||
deletedFeatures={[deletedFeature?.name!]}
|
||||
projectId={projectId!}
|
||||
open={deleteModalOpen}
|
||||
setOpen={setDeleteModalOpen}
|
||||
refetch={refetch}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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 “
|
||||
{searchValue}”
|
||||
</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
|
||||
condition={Boolean(projectId)}
|
||||
show={
|
||||
<BatchSelectionActionsBar
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
>
|
||||
<ArchiveBatchActions
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
@ -360,6 +363,6 @@ export const ArchiveTable = ({
|
||||
</BatchSelectionActionsBar>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,14 +2,21 @@ import { FC } from 'react';
|
||||
import { Box, Paper, styled, Typography } from '@mui/material';
|
||||
|
||||
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',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
paddingBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledBar = styled(Paper)(({ theme }) => ({
|
||||
@ -40,22 +47,24 @@ const StyledText = styled(Typography)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export const BatchSelectionActionsBar: FC<IBatchSelectionActionsBarProps> = ({
|
||||
selectedIds,
|
||||
count,
|
||||
children,
|
||||
}) => {
|
||||
if (selectedIds.length === 0) {
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledBar elevation={4}>
|
||||
<StyledText>
|
||||
<StyledCount>{selectedIds.length}</StyledCount>
|
||||
 selected
|
||||
</StyledText>
|
||||
{children}
|
||||
</StyledBar>
|
||||
</StyledContainer>
|
||||
<StyledStickyContainer>
|
||||
<StyledContainer>
|
||||
<StyledBar elevation={4}>
|
||||
<StyledText>
|
||||
<StyledCount>{count}</StyledCount>
|
||||
 selected
|
||||
</StyledText>
|
||||
{children}
|
||||
</StyledBar>
|
||||
</StyledContainer>
|
||||
</StyledStickyContainer>
|
||||
);
|
||||
};
|
||||
|
@ -91,7 +91,6 @@ export const InstanceStatus: FC = ({ children }) => {
|
||||
useInstanceStatus();
|
||||
const { extendTrial } = useInstanceStatusApi();
|
||||
const { setToastApiError } = useToast();
|
||||
const theme = useTheme();
|
||||
|
||||
const onExtendTrial = async () => {
|
||||
try {
|
||||
@ -103,12 +102,7 @@ export const InstanceStatus: FC = ({ children }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={isBilling && Boolean(instanceStatus)}
|
||||
show={() => (
|
||||
@ -124,7 +118,7 @@ export const InstanceStatus: FC = ({ children }) => {
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -21,12 +21,15 @@ interface IMainLayoutProps {
|
||||
const MainLayoutContainer = styled(Grid)(() => ({
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
const MainLayoutContentWrapper = styled('main')(({ theme }) => ({
|
||||
margin: theme.spacing(0, 'auto'),
|
||||
overflow: 'auto', // prevent margin collapsing
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.background.application,
|
||||
position: 'relative',
|
||||
|
@ -554,174 +554,187 @@ export const ProjectFeatureToggles = ({
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
className={styles.container}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={`Feature toggles (${rows.length})`}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ColumnsMenu
|
||||
allColumns={allColumns}
|
||||
staticColumns={staticColumns}
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={Boolean(storedParams.columns)}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
/>
|
||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig?.flags?.featuresExportImport
|
||||
)}
|
||||
show={
|
||||
<Tooltip
|
||||
title="Export toggles visible in the table below"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setShowExportDialog(true)
|
||||
}
|
||||
sx={theme => ({
|
||||
marginRight: theme.spacing(2),
|
||||
})}
|
||||
<>
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
className={styles.container}
|
||||
header={
|
||||
<PageHeader
|
||||
titleElement={`Feature toggles (${rows.length})`}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ColumnsMenu
|
||||
allColumns={allColumns}
|
||||
staticColumns={staticColumns}
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={Boolean(storedParams.columns)}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
/>
|
||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig?.flags?.featuresExportImport
|
||||
)}
|
||||
show={
|
||||
<Tooltip
|
||||
title="Export toggles visible in the table below"
|
||||
arrow
|
||||
>
|
||||
<FileDownload />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<StyledResponsiveButton
|
||||
onClick={() =>
|
||||
navigate(getCreateTogglePath(projectId))
|
||||
}
|
||||
maxWidth="960px"
|
||||
Icon={Add}
|
||||
projectId={projectId}
|
||||
permission={CREATE_FEATURE}
|
||||
data-testid="NAVIGATE_TO_CREATE_FEATURE"
|
||||
>
|
||||
New feature toggle
|
||||
</StyledResponsiveButton>
|
||||
</>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setShowExportDialog(true)
|
||||
}
|
||||
sx={theme => ({
|
||||
marginRight:
|
||||
theme.spacing(2),
|
||||
})}
|
||||
>
|
||||
<FileDownload />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<StyledResponsiveButton
|
||||
onClick={() =>
|
||||
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 “
|
||||
{searchValue}
|
||||
”
|
||||
</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>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchValue?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No feature toggles found matching “
|
||||
{searchValue}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No feature toggles available. Get started by
|
||||
adding a new feature toggle.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EnvironmentStrategyDialog
|
||||
onClose={() =>
|
||||
setStrategiesDialogState(prev => ({ ...prev, open: false }))
|
||||
}
|
||||
projectId={projectId}
|
||||
{...strategiesDialogState}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
onClose={() => {
|
||||
setFeatureStaleDialogState({});
|
||||
refetch();
|
||||
}}
|
||||
featureId={featureStaleDialogState.featureId || ''}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureArchiveDialog
|
||||
isOpen={Boolean(featureArchiveState)}
|
||||
onConfirm={() => {
|
||||
refetch();
|
||||
}}
|
||||
onClose={() => {
|
||||
setFeatureArchiveState(undefined);
|
||||
}}
|
||||
featureIds={[featureArchiveState || '']}
|
||||
projectId={projectId}
|
||||
/>{' '}
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
messageComponent={
|
||||
<UpdateEnabledMessage
|
||||
featureName={changeRequestDialogDetails.featureName!}
|
||||
enabled={changeRequestDialogDetails.enabled!}
|
||||
environment={changeRequestDialogDetails?.environment!}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
Boolean(uiConfig?.flags?.featuresExportImport) && !loading
|
||||
}
|
||||
show={
|
||||
<ExportDialog
|
||||
showExportDialog={showExportDialog}
|
||||
data={data}
|
||||
onClose={() => setShowExportDialog(false)}
|
||||
environments={environments}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<BatchSelectionActionsBar selectedIds={Object.keys(selectedRowIds)}>
|
||||
<EnvironmentStrategyDialog
|
||||
onClose={() =>
|
||||
setStrategiesDialogState(prev => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}))
|
||||
}
|
||||
projectId={projectId}
|
||||
{...strategiesDialogState}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
onClose={() => {
|
||||
setFeatureStaleDialogState({});
|
||||
refetch();
|
||||
}}
|
||||
featureId={featureStaleDialogState.featureId || ''}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureArchiveDialog
|
||||
isOpen={Boolean(featureArchiveState)}
|
||||
onConfirm={() => {
|
||||
refetch();
|
||||
}}
|
||||
onClose={() => {
|
||||
setFeatureArchiveState(undefined);
|
||||
}}
|
||||
featureIds={[featureArchiveState || '']}
|
||||
projectId={projectId}
|
||||
/>{' '}
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestToggleClose}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestToggleConfirm}
|
||||
messageComponent={
|
||||
<UpdateEnabledMessage
|
||||
featureName={
|
||||
changeRequestDialogDetails.featureName!
|
||||
}
|
||||
enabled={changeRequestDialogDetails.enabled!}
|
||||
environment={
|
||||
changeRequestDialogDetails?.environment!
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
||||
!loading
|
||||
}
|
||||
show={
|
||||
<ExportDialog
|
||||
showExportDialog={showExportDialog}
|
||||
data={data}
|
||||
onClose={() => setShowExportDialog(false)}
|
||||
environments={environments}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
<BatchSelectionActionsBar
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
>
|
||||
<ProjectFeaturesBatchActions
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
data={features}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</BatchSelectionActionsBar>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -9,10 +9,13 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
font-family: 'Sen', sans-serif;
|
||||
font-size: 16px;
|
||||
font-variant-ligatures: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
@ -139,7 +142,10 @@ a:hover {
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.MuiCardHeader-title {
|
||||
|
Loading…
Reference in New Issue
Block a user