Skip to content

Commit 22d72d8

Browse files
katinthehatsiteKateryna Kodonenkosejas
authored
Studio: Show sync selection size (e.g. 1.2 GB / 2 GB) in site push dialog (#2125)
* Add basic implementation * Account for footer sizing * Add custom progress bar * Minor styling * Final adjustments for the style * Component refactor * Ensure we use color variables * Rename bytes * Add total limit on the right * Make the file size calculation recursive * Add special condition for mu-plugins * For mu-plugins calculate it only for selected nodes because we have sqlite mu-plugin in the file-system * Reuse SYNC_PUSH_SIZE_LIMIT_BYTES constant * Fix tests by making wpContentIndeterminate more real --------- Co-authored-by: Kateryna Kodonenko <[email protected]> Co-authored-by: Antonio Sejas <[email protected]>
1 parent 59daed8 commit 22d72d8

File tree

4 files changed

+172
-40
lines changed

4 files changed

+172
-40
lines changed

src/components/progress-bar.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,83 @@ export function ProgressBarWithAutoIncrement( {
6060

6161
return <ProgressBar value={ animatedValue } maxValue={ maxValue } />;
6262
}
63+
64+
type TwoColorProgressBarProps = {
65+
value: number;
66+
maxValue: number;
67+
normalColorClass?: string;
68+
overLimitColorClass?: string;
69+
trackColorClass?: string;
70+
showLabels?: boolean;
71+
valueLabel?: string;
72+
limitLabel?: string;
73+
overLimitLabel?: string;
74+
};
75+
76+
export function TwoColorProgressBar( {
77+
value,
78+
maxValue,
79+
normalColorClass = 'bg-a8c-blue-50',
80+
overLimitColorClass = 'bg-a8c-red-50',
81+
trackColorClass = 'bg-a8c-gray-5',
82+
showLabels = false,
83+
valueLabel,
84+
limitLabel,
85+
overLimitLabel,
86+
}: TwoColorProgressBarProps ) {
87+
const isOverLimit = value > maxValue;
88+
const percentage = Math.min( ( value / maxValue ) * 100, 100 );
89+
90+
return (
91+
<div>
92+
{ showLabels && ( valueLabel || limitLabel || overLimitLabel ) && (
93+
<div className="flex justify-between items-center text-xs mb-2">
94+
<div className="text-a8c-gray-90 font-medium uppercase">{ valueLabel }</div>
95+
<div>
96+
{ isOverLimit && overLimitLabel ? (
97+
<span className="text-a8c-gray-700 text-xs">{ overLimitLabel }</span>
98+
) : (
99+
limitLabel && <span className="text-a8c-gray-700 text-xs">{ limitLabel }</span>
100+
) }
101+
</div>
102+
</div>
103+
) }
104+
<div
105+
className={ cx(
106+
'relative w-full h-[1.5px] rounded-full overflow-hidden',
107+
trackColorClass
108+
) }
109+
>
110+
{ isOverLimit ? (
111+
<>
112+
<div
113+
className={ cx(
114+
'absolute left-0 top-0 h-full transition-all duration-300',
115+
normalColorClass
116+
) }
117+
style={ { width: `${ ( maxValue / value ) * 100 }%` } }
118+
/>
119+
<div
120+
className={ cx(
121+
'absolute top-0 h-full transition-all duration-300',
122+
overLimitColorClass
123+
) }
124+
style={ {
125+
left: `${ ( maxValue / value ) * 100 }%`,
126+
width: `${ ( ( value - maxValue ) / value ) * 100 }%`,
127+
} }
128+
/>
129+
</>
130+
) : (
131+
<div
132+
className={ cx(
133+
'absolute left-0 top-0 h-full rounded-full transition-all duration-300',
134+
normalColorClass
135+
) }
136+
style={ { width: `${ percentage }%` } }
137+
/>
138+
) }
139+
</div>
140+
</div>
141+
);
142+
}

src/modules/sync/components/sync-dialog.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ArrowIcon } from 'src/components/arrow-icon';
88
import Button from 'src/components/button';
99
import { RightArrowIcon } from 'src/components/icons/right-arrow';
1010
import Modal from 'src/components/modal';
11+
import { TwoColorProgressBar } from 'src/components/progress-bar';
1112
import { Tooltip } from 'src/components/tooltip';
1213
import { TreeView, TreeNode, updateNodeById } from 'src/components/tree-view';
1314
import { SYNC_PUSH_SIZE_LIMIT_GB } from 'src/constants';
@@ -140,11 +141,15 @@ export function SyncDialog( {
140141
const [ showAllFiles, setShowAllFiles ] = useState( false );
141142
const [ treeState, setTreeState ] = useState< TreeNode[] >( defaultTree );
142143
const isSubmitDisabled = treeState.every( ( node ) => ! node.checked && ! node.indeterminate );
143-
const { isPushSelectionOverLimit, isLoading: isSizeCheckLoading } = useSelectedItemsPushSize(
144-
localSite.id,
145-
treeState,
146-
type
147-
);
144+
const {
145+
isPushSelectionOverLimit,
146+
isLoading: isSizeCheckLoading,
147+
totalSize,
148+
limitBytes,
149+
formattedSize,
150+
formattedLimit,
151+
formattedOverAmount,
152+
} = useSelectedItemsPushSize( localSite.id, treeState, type );
148153

149154
const { fetchChildren, rewindId, isLoadingRewindId, isErrorRewindId, isLoadingLocalFileTree } =
150155
useDynamicTreeState( type, localSite.id, remoteSite.id, setTreeState );
@@ -218,13 +223,23 @@ export function SyncDialog( {
218223
onRequestClose();
219224
};
220225

226+
const getBottomPadding = () => {
227+
if ( type === 'pull' ) {
228+
return 'pb-[70px]'; // Original padding for pull
229+
}
230+
if ( isPushSelectionOverLimit ) {
231+
return 'pb-[200px]'; // Progress bar + warning notice
232+
}
233+
return 'pb-[110px]'; // Just progress bar
234+
};
235+
221236
return (
222237
<Modal
223238
className="w-3/5 min-w-[550px] max-h-[84vh] [&>div]:!p-0"
224239
onRequestClose={ onRequestClose }
225240
title={ syncTexts.title }
226241
>
227-
<div className={ isPushSelectionOverLimit ? 'pb-[140px]' : 'pb-[70px]' }>
242+
<div className={ getBottomPadding() }>
228243
<div className="px-8 pb-6 pt-1">{ syncTexts.description }</div>
229244
<div className="px-8">
230245
<span className="sr-only">
@@ -321,7 +336,19 @@ export function SyncDialog( {
321336
</div>
322337
</Tooltip>
323338

324-
<div className="px-8 py-4 absolute left-0 right-0 bottom-0 bg-white z-10">
339+
<div className="px-8 py-4 absolute left-0 right-0 bottom-0 bg-white z-10 border-t border-a8c-gray-5">
340+
{ type === 'push' && (
341+
<div className="mb-4">
342+
<TwoColorProgressBar
343+
value={ totalSize }
344+
maxValue={ limitBytes }
345+
showLabels
346+
valueLabel={ formattedSize }
347+
limitLabel={ formattedLimit }
348+
overLimitLabel={ sprintf( __( '%s over' ), formattedOverAmount ) }
349+
/>
350+
</div>
351+
) }
325352
{ type === 'push' && isPushSelectionOverLimit && (
326353
<Notice status="warning" isDismissible={ false } className="mb-4">
327354
<p data-testid="push-selection-over-limit-notice">

src/modules/sync/hooks/use-selected-items-push-size.ts

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,27 @@ import { TreeNode } from 'src/components/tree-view';
33
import { SYNC_PUSH_SIZE_LIMIT_BYTES } from 'src/constants';
44
import { getIpcApi } from 'src/lib/get-ipc-api';
55

6+
const formatFileSize = ( bytes: number ) => {
7+
if ( bytes === 0 ) return '0 B';
8+
const k = 1024;
9+
const sizes = [ 'B', 'KB', 'MB', 'GB' ];
10+
const i = Math.floor( Math.log( bytes ) / Math.log( k ) );
11+
return Math.round( ( bytes / Math.pow( k, i ) ) * 100 ) / 100 + ' ' + sizes[ i ];
12+
};
13+
614
export function useSelectedItemsPushSize(
715
siteId: string,
816
treeState: TreeNode[],
917
type: 'push' | 'pull'
1018
) {
1119
const [ isPushSelectionOverLimit, setIsPushSelectionOverLimit ] = useState( false );
1220
const [ isLoading, setIsLoading ] = useState( false );
21+
const [ totalSize, setTotalSize ] = useState( 0 );
1322

1423
const checkSelectedItemsSize = useCallback( async () => {
1524
if ( ! siteId || ! treeState.length || type !== 'push' ) {
1625
setIsPushSelectionOverLimit( false );
26+
setTotalSize( 0 );
1727
return;
1828
}
1929

@@ -24,6 +34,7 @@ export function useSelectedItemsPushSize(
2434

2535
if ( isEverythingSelected ) {
2636
const size = await getIpcApi().getDirectorySize( siteId, [ 'wp-content' ] );
37+
setTotalSize( size );
2738
setIsPushSelectionOverLimit( size > SYNC_PUSH_SIZE_LIMIT_BYTES );
2839
return;
2940
}
@@ -40,52 +51,47 @@ export function useSelectedItemsPushSize(
4051
( node ) => node.id === 'wp-content'
4152
);
4253

43-
if ( wpContentNode?.checked ) {
44-
sizePromises.push( getIpcApi().getDirectorySize( siteId, [ 'wp-content' ] ) );
45-
} else if ( wpContentNode?.children ) {
46-
for ( const child of wpContentNode.children ) {
47-
if ( child.checked ) {
48-
if ( child.type === 'file' ) {
49-
sizePromises.push( getIpcApi().getFileSize( siteId, [ 'wp-content', child.name ] ) );
54+
const processNodeRecursively = ( node: TreeNode, pathPrefix: string[] ): void => {
55+
if ( node.checked ) {
56+
// If the node is fully checked, get its entire size
57+
if ( node.type === 'file' ) {
58+
sizePromises.push( getIpcApi().getFileSize( siteId, [ ...pathPrefix, node.name ] ) );
59+
} else {
60+
if ( node.name === 'mu-plugins' && node.children ) {
61+
for ( const child of node.children ) {
62+
processNodeRecursively( child, [ ...pathPrefix, node.name ] );
63+
}
5064
} else {
5165
sizePromises.push(
52-
getIpcApi().getDirectorySize( siteId, [ 'wp-content', child.name ] )
66+
getIpcApi().getDirectorySize( siteId, [ ...pathPrefix, node.name ] )
5367
);
5468
}
55-
} else if ( child.indeterminate && child.children ) {
56-
for ( const subChild of child.children ) {
57-
if ( subChild.checked || subChild.indeterminate ) {
58-
let sizePromise: Promise< number >;
59-
if ( subChild.type === 'file' ) {
60-
sizePromise = getIpcApi().getFileSize( siteId, [
61-
'wp-content',
62-
child.name,
63-
subChild.name,
64-
] );
65-
} else {
66-
sizePromise = getIpcApi().getDirectorySize( siteId, [
67-
'wp-content',
68-
child.name,
69-
subChild.name,
70-
] );
71-
}
72-
sizePromises.push( sizePromise );
73-
}
74-
}
69+
}
70+
} else if ( node.indeterminate && node.children ) {
71+
// If the node is indeterminate, process its children recursively
72+
for ( const child of node.children ) {
73+
processNodeRecursively( child, [ ...pathPrefix, node.name ] );
7574
}
7675
}
76+
};
77+
78+
if ( wpContentNode ) {
79+
processNodeRecursively( wpContentNode, [] );
7780
}
7881

7982
if ( sizePromises.length > 0 ) {
8083
const sizes = await Promise.all( sizePromises );
81-
const totalSize = sizes.reduce( ( sum, size ) => sum + size, 0 );
82-
setIsPushSelectionOverLimit( totalSize > SYNC_PUSH_SIZE_LIMIT_BYTES );
84+
const calculatedSize = sizes.reduce( ( sum, size ) => sum + size, 0 );
85+
setTotalSize( calculatedSize );
86+
setIsPushSelectionOverLimit( calculatedSize > SYNC_PUSH_SIZE_LIMIT_BYTES );
8387
} else {
88+
setTotalSize( 0 );
8489
setIsPushSelectionOverLimit( false );
8590
}
8691
} catch ( error ) {
8792
console.error( 'Error checking selected items size:', error );
8893
setIsPushSelectionOverLimit( false );
94+
setTotalSize( 0 );
8995
} finally {
9096
setIsLoading( false );
9197
}
@@ -96,5 +102,16 @@ export function useSelectedItemsPushSize(
96102
void checkSelectedItemsSize();
97103
}, [ checkSelectedItemsSize ] );
98104

99-
return { isPushSelectionOverLimit, isLoading };
105+
const limitBytes = SYNC_PUSH_SIZE_LIMIT_BYTES;
106+
const overAmount = totalSize > limitBytes ? totalSize - limitBytes : 0;
107+
108+
return {
109+
isPushSelectionOverLimit,
110+
isLoading,
111+
totalSize,
112+
limitBytes,
113+
formattedSize: formatFileSize( totalSize ),
114+
formattedLimit: formatFileSize( limitBytes ),
115+
formattedOverAmount: formatFileSize( overAmount ),
116+
};
100117
}

src/modules/sync/tests/use-selected-items-push-size.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const createMockTreeState = (
2222
allSelected = false,
2323
} = options;
2424

25+
const wpContentIndeterminate =
26+
! filesAndFoldersSelected &&
27+
! allSelected &&
28+
( options.pluginsSelected || options.themesSelected || options.uploadsSelected );
29+
2530
return [
2631
{
2732
id: 'filesAndFolders',
@@ -37,7 +42,7 @@ const createMockTreeState = (
3742
name: 'wp-content',
3843
label: 'wp-content',
3944
checked: allSelected || filesAndFoldersSelected,
40-
indeterminate: false,
45+
indeterminate: wpContentIndeterminate,
4146
type: 'folder',
4247
children: [
4348
{
@@ -151,7 +156,10 @@ describe( 'useSelectedItemsPushSize Hook Tests', () => {
151156
getDirectorySize: mockGetDirectorySize,
152157
} );
153158

154-
const treeState = createMockTreeState( { pluginsSelected: true, themesSelected: true } );
159+
const treeState = createMockTreeState( {
160+
pluginsSelected: true,
161+
themesSelected: true,
162+
} );
155163

156164
const { result } = renderHook( () =>
157165
useSelectedItemsPushSize( mockSiteId, treeState, 'push' )

0 commit comments

Comments
 (0)