-
Notifications
You must be signed in to change notification settings - Fork 88
Agentic UI: Add backup import to the site overview #4500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
be62a33
Add backup import to the Agentic UI site overview
bcotrim c4ca566
Exclude .sql dumps from Agentic UI site creation
bcotrim 01c1766
Merge branch 'trunk' into stu-2008-add-import-export-to-agentic-ui
bcotrim 9050b9d
Correct import comments that assumed a freshly created site
bcotrim e0efb72
Merge remote-tracking branch 'origin/stu-2008-add-import-export-to-ag…
bcotrim d0abc31
Use AlertDialog for the import overwrite confirmation
bcotrim 9d27c4b
Throttle import progress toasts and soften the overwrite confirm button
bcotrim b1a5fed
Track import progress per site so it doesn't follow navigation
bcotrim 221b5b1
Shorten import status messages so the toast stays on one line
bcotrim c08c2c7
Clamp the import progress toast to one line for longer translations
bcotrim ef6b27b
Pad the import percentage to two digits instead of clamping the toast
bcotrim 5004ebb
Use tabular figures in toasts so the import percentage doesn't shift
bcotrim eb95315
Show import progress per site instead of in a global toast
bcotrim 2c2fb67
Lead sync and import progress labels with a padded percentage
bcotrim a92fa5c
Refetch site details invalidated by an import
bcotrim 52f2d56
Merge remote-tracking branch 'origin/trunk' into stu-2008-add-import-…
bcotrim 4ba390e
Drop redundant comments from the site overview tests
bcotrim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import { ACCEPTED_IMPORT_FILE_TYPES } from '@studio/common/constants'; | ||
| import { isSupportedBackupFilename } from '@studio/common/lib/backup-files'; | ||
| import { getErrorMessage } from '@studio/common/lib/error-formatting'; | ||
| import { getImportStatusMessage } from '@studio/common/lib/import-progress'; | ||
| import { __, sprintf } from '@wordpress/i18n'; | ||
| import { AlertDialog } from '@wordpress/ui'; | ||
| import { useState } from 'react'; | ||
| import { toast } from '@/data/app-messages'; | ||
| import { useConnector } from '@/data/core'; | ||
| import { useImportSite } from '@/data/queries/use-import-site'; | ||
| import { | ||
| reportSyncError, | ||
| reportSyncPending, | ||
| reportSyncProgress, | ||
| reportSyncSuccess, | ||
| useSiteSyncActivity, | ||
| } from '@/data/sync-activity'; | ||
| import styles from './style.module.css'; | ||
| import type { SiteDetails } from '@/data/core'; | ||
|
|
||
| export const IMPORT_FILE_ACCEPT = ACCEPTED_IMPORT_FILE_TYPES.join( ',' ); | ||
|
|
||
| // `confirming` is tracked alongside the file rather than derived from it because | ||
| // the popup stays mounted through its closing animation — dropping the file to | ||
| // close would shrink the dialog mid-fade. | ||
| interface PendingImport { | ||
| siteId: string; | ||
| file: File; | ||
| confirming: boolean; | ||
| } | ||
|
|
||
| export function useSiteBackupImport( site: SiteDetails ) { | ||
| const connector = useConnector(); | ||
| const importSite = useImportSite(); | ||
| // Everything here is stamped with a site id: the overview stays mounted when | ||
| // the user switches sites (the route only swaps the `$siteId` param), so a | ||
| // plain boolean would follow them and light up the next site's Import button. | ||
| const [ pending, setPending ] = useState< PendingImport | null >( null ); | ||
|
|
||
| // The activity store is keyed by site and lives outside React, so progress | ||
| // survives navigating away and shows on whichever surface renders this site. | ||
| const activity = useSiteSyncActivity( site.id ); | ||
|
|
||
| const active = pending?.siteId === site.id ? pending : null; | ||
| const isImporting = activity?.kind === 'pending' && activity.direction === 'import'; | ||
|
|
||
| const selectFile = ( picked?: File ) => { | ||
| if ( ! picked ) { | ||
| return; | ||
| } | ||
| // The input's `accept` filter is advisory — a drag or an "All files" | ||
| // pick can still hand us something unsupported. | ||
| if ( ! isSupportedBackupFilename( picked.name ) ) { | ||
| toast.error( | ||
| __( | ||
| 'This file type is not supported. Please use a .zip, .gz, .gzip, .tar, .tar.gz, .wpress, .sql, or .xml file.' | ||
| ) | ||
| ); | ||
| return; | ||
| } | ||
| setPending( { siteId: site.id, file: picked, confirming: true } ); | ||
| }; | ||
|
|
||
| const closeDialog = () => | ||
| setPending( ( current ) => | ||
| current?.siteId === site.id ? { ...current, confirming: false } : current | ||
| ); | ||
|
|
||
| const confirm = async () => { | ||
| const file = active?.file; | ||
| if ( ! file || isImporting ) { | ||
| return; | ||
| } | ||
| const { id: siteId } = site; | ||
| closeDialog(); | ||
| reportSyncPending( siteId, 'import' ); | ||
| // Extraction reports progress once per stream chunk, so a large backup | ||
| // fires thousands of events a second. Only report when the rendered text | ||
| // actually changes — otherwise the store notifies its subscribers that | ||
| // fast and the app stops responding to clicks. | ||
| let lastMessage = ''; | ||
| try { | ||
| const backupPath = await connector.getFilePath( file ); | ||
| if ( ! backupPath ) { | ||
| throw new Error( __( 'Unable to access the selected backup. Please try again.' ) ); | ||
| } | ||
| await importSite.mutateAsync( { | ||
| siteId, | ||
| backupPath, | ||
| onProgress: ( event ) => { | ||
| const message = getImportStatusMessage( event ); | ||
| if ( message && message !== lastMessage ) { | ||
| lastMessage = message; | ||
| reportSyncProgress( siteId, 'import', { message } ); | ||
| } | ||
| }, | ||
| } ); | ||
| reportSyncSuccess( siteId, 'import' ); | ||
| } catch ( error ) { | ||
| // Matches push/pull: the activity store carries the detail on the site | ||
| // itself, and a toast says so wherever the user has navigated to. | ||
| const message = | ||
| getErrorMessage( error ) ?? __( 'Failed to import the backup. Please try again.' ); | ||
| reportSyncError( siteId, 'import', message ); | ||
| toast.error( __( "Import didn't complete" ), { description: message } ); | ||
| } finally { | ||
| // Drop the File so a large backup isn't held in memory for the session. | ||
| setPending( ( current ) => ( current?.siteId === siteId ? null : current ) ); | ||
| } | ||
| }; | ||
|
|
||
| return { | ||
| file: active?.file ?? null, | ||
| isConfirming: active?.confirming ?? false, | ||
| selectFile, | ||
| cancel: closeDialog, | ||
| confirm, | ||
| isImporting, | ||
| }; | ||
| } | ||
|
|
||
| interface ImportSiteDialogProps { | ||
| site: SiteDetails; | ||
| file: File | null; | ||
| open: boolean; | ||
| onCancel: () => void; | ||
| onConfirm: () => void; | ||
| } | ||
|
|
||
| export function ImportSiteDialog( { | ||
| site, | ||
| file, | ||
| open, | ||
| onCancel, | ||
| onConfirm, | ||
| }: ImportSiteDialogProps ) { | ||
| return ( | ||
| <AlertDialog.Root | ||
| open={ open } | ||
| onOpenChange={ ( next ) => { | ||
| if ( ! next ) { | ||
| onCancel(); | ||
| } | ||
| } } | ||
| // Returns synchronously so the dialog closes and the import runs in the | ||
| // background — an async handler would hold it open for the whole import. | ||
| onConfirm={ onConfirm } | ||
| > | ||
| { /* Deliberately not `intent="irreversible"`: the importer moves the site's | ||
| existing wp-content and database to the trash, not straight to deletion. */ } | ||
| <AlertDialog.Popup | ||
| title={ sprintf( __( 'Overwrite %s?' ), site.name ) } | ||
| description={ __( | ||
| 'Importing a backup will replace the existing files and database for your site.' | ||
| ) } | ||
| confirmButtonText={ __( 'Import' ) } | ||
| > | ||
| { file ? <p className={ styles.fileName }>{ file.name }</p> : null } | ||
| </AlertDialog.Popup> | ||
| </AlertDialog.Root> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .fileName { | ||
| margin: var(--wpds-dimension-padding-sm) 0 0; | ||
| font-size: var(--wpds-typography-font-size-sm); | ||
| line-height: var(--wpds-typography-line-height-sm); | ||
| color: var(--wpds-color-fg-content-neutral); | ||
| overflow-wrap: anywhere; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure whether this button needs to have destructive styling:
It seems a bit intense there, what do you think? Although can be left as is if that's the intention