diff --git a/src/components/content-tab-previews.tsx b/src/components/content-tab-previews.tsx new file mode 100644 index 000000000..69da1a362 --- /dev/null +++ b/src/components/content-tab-previews.tsx @@ -0,0 +1,208 @@ +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { check, external, Icon } from '@wordpress/icons'; +import { useI18n } from '@wordpress/react-i18n'; +import { PropsWithChildren } from 'react'; +import { + CLIENT_ID, + PROTOCOL_PREFIX, + SCOPES, + WP_AUTHORIZE_ENDPOINT, + LIMIT_OF_ZIP_SITES_PER_USER, +} from 'src/constants'; +import { useArchiveSite } from 'src/hooks/use-archive-site'; +import { useAuth } from 'src/hooks/use-auth'; +import { useOffline } from 'src/hooks/use-offline'; +import { useSnapshots } from 'src/hooks/use-snapshots'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import { CreatePreviewButton } from 'src/modules/preview-site/components/create-preview-button'; +import { PreviewSiteRow } from 'src/modules/preview-site/components/preview-site-row'; +import { PreviewSitesTableHeader } from 'src/modules/preview-site/components/preview-sites-table-header'; +import { ProgressRow } from 'src/modules/preview-site/components/progress-row'; +import Button from './button'; +import offlineIcon from './offline-icon'; +import { ScreenshotDemoSite } from './screenshot-demo-site'; +import { Tooltip } from './tooltip'; + +interface ContentTabPreviewsProps { + selectedSite: SiteDetails; +} + +function EmptyGeneric( { + children, + selectedSite, +}: PropsWithChildren< { selectedSite: SiteDetails } > ) { + const { __ } = useI18n(); + return ( +
+
+
{ __( 'Share a preview of your Studio site' ) }
+
+ { __( + 'Get feedback from anyone, anywhere with a free hosted preview of your Studio site.' + ) } +
+
+ { [ + __( `Create up to ${ LIMIT_OF_ZIP_SITES_PER_USER } preview sites for free.` ), + __( 'Preview sites expire 7 days after the last update.' ), + createInterpolateElement( __( 'Powered by WordPress.com.' ), { + a: ( +
+ { children } +
+
+ +
+
+ ); +} + +function NoAuth( { selectedSite }: React.ComponentProps< typeof EmptyGeneric > ) { + const isOffline = useOffline(); + const { __ } = useI18n(); + const { authenticate } = useAuth(); + const offlineMessage = __( "You're currently offline." ); + + return ( + +
+ + + +
+
+ + { createInterpolateElement( + __( + 'A WordPress.com account is required to create preview sites. Create a free account' + ), + { + a: ( +
+
+ ); +} + +function NoPreviews( { selectedSite }: React.ComponentProps< typeof EmptyGeneric > ) { + const { archiveSite } = useArchiveSite(); + + return ( + +
+ archiveSite( selectedSite.id ) } + selectedSite={ selectedSite } + /> +
+
+ ); +} + +export function ContentTabPreviews( { selectedSite }: ContentTabPreviewsProps ) { + const { __ } = useI18n(); + const { snapshots } = useSnapshots(); + const { isAuthenticated } = useAuth(); + const { archiveSite, isUploadingSiteId } = useArchiveSite(); + const isUploading = isUploadingSiteId( selectedSite.id ); + const snapshotsOnSite = snapshots.filter( + ( snapshot ) => snapshot.localSiteId === selectedSite.id + ); + const isSnapshotLoading = snapshotsOnSite.some( ( snapshot ) => snapshot.isLoading ); + + if ( ! isAuthenticated ) { + return ; + } + + if ( ! snapshotsOnSite.length && ! isUploading && ! isSnapshotLoading ) { + return ; + } + + return ( +
+
+
+ +
+ { ( isUploading || isSnapshotLoading ) && ( + + ) } + { snapshotsOnSite + .filter( ( snapshot ) => ! snapshot.isLoading ) + .map( ( snapshot ) => ( + + ) ) } +
+
+
+ archiveSite( selectedSite.id ) } + selectedSite={ selectedSite } + /> +
+
+
+ ); +} diff --git a/src/components/content-tab-snapshots.tsx b/src/components/content-tab-snapshots.tsx index 16958402e..c61ef8535 100644 --- a/src/components/content-tab-snapshots.tsx +++ b/src/components/content-tab-snapshots.tsx @@ -67,7 +67,7 @@ function SnapshotRow( { const isUploading = isUploadingSiteId( selectedSite.id ); const { updateDemoSite, isDemoSiteUpdating } = useUpdateDemoSite(); const errorMessages = useArchiveErrorMessages(); - const isSiteDemoUpdating = isDemoSiteUpdating( snapshot.localSiteId ); + const isSiteDemoUpdating = isDemoSiteUpdating( snapshot.atomicSiteId ); const { formatRelativeTime } = useFormatLocalizedTimestamps(); const { isOverLimit } = useSiteSize( selectedSite.id ); diff --git a/src/components/site-content-tabs.tsx b/src/components/site-content-tabs.tsx index 8bb6ed88e..cfe3a2386 100644 --- a/src/components/site-content-tabs.tsx +++ b/src/components/site-content-tabs.tsx @@ -7,6 +7,7 @@ import { WelcomeMessagesProvider } from '../hooks/use-welcome-messages'; import { ContentTabAssistant } from './content-tab-assistant'; import { ContentTabImportExport } from './content-tab-import-export'; import { ContentTabOverview } from './content-tab-overview'; +import { ContentTabPreviews } from './content-tab-previews'; import { ContentTabSettings } from './content-tab-settings'; import { ContentTabSnapshots } from './content-tab-snapshots'; import { ContentTabSync } from './content-tab-sync'; @@ -47,6 +48,7 @@ export function SiteContentTabs() {
{ name === 'overview' && } { name === 'share' && } + { name === 'previews' && } { name === 'sync' && } { name === 'settings' && } { name === 'assistant' && ( diff --git a/src/hooks/tests/use-update-demo-site.test.tsx b/src/hooks/tests/use-update-demo-site.test.tsx index 7b0c2540a..83b5fba88 100644 --- a/src/hooks/tests/use-update-demo-site.test.tsx +++ b/src/hooks/tests/use-update-demo-site.test.tsx @@ -108,7 +108,7 @@ describe( 'useUpdateDemoSite', () => { } ); // Assert that 'isDemoSiteUpdating' is set back to false - expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false ); + expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false ); // Assert that demo site is updated with a new expiration date expect( updateSnapshotMock ).toHaveBeenCalledWith( @@ -144,7 +144,7 @@ describe( 'useUpdateDemoSite', () => { } ); // Assert that 'isDemoSiteUpdating' is set back to false - expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false ); + expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false ); } ); it( 'should allow updating two sites independently with different completion times', async () => { @@ -184,8 +184,8 @@ describe( 'useUpdateDemoSite', () => { } ); // Initially, both sites should be marked as updating - expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( true ); - expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( true ); + expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( true ); + expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( true ); // Wait for the first site to complete await act( async () => { @@ -194,8 +194,8 @@ describe( 'useUpdateDemoSite', () => { } ); // After 1000ms, the first site should be done, but the second should still be updating - expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false ); - expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( true ); + expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false ); + expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( true ); // Wait for the second site to complete await act( async () => { @@ -204,8 +204,8 @@ describe( 'useUpdateDemoSite', () => { } ); // After another 1000ms, both sites should be done updating - expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false ); - expect( result.current.isDemoSiteUpdating( mockLocalSite2.id ) ).toBe( false ); + expect( result.current.isDemoSiteUpdating( mockSnapshot.atomicSiteId ) ).toBe( false ); + expect( result.current.isDemoSiteUpdating( mockSnapshot2.atomicSiteId ) ).toBe( false ); // Assert that the update function was called for both sites expect( clientReqPost ).toHaveBeenCalledTimes( 2 ); diff --git a/src/hooks/use-confirmation-dialog.ts b/src/hooks/use-confirmation-dialog.ts index 6d134d831..04ea66744 100644 --- a/src/hooks/use-confirmation-dialog.ts +++ b/src/hooks/use-confirmation-dialog.ts @@ -7,7 +7,8 @@ interface ConfirmationDialogOptions { checkboxLabel?: string; confirmButtonLabel: string; cancelButtonLabel?: string; - localStorageKey: string; + type?: 'none' | 'info' | 'error' | 'question' | 'warning'; + localStorageKey?: string; } export function useConfirmationDialog( options: ConfirmationDialogOptions ) { @@ -19,10 +20,11 @@ export function useConfirmationDialog( options: ConfirmationDialogOptions ) { confirmButtonLabel, cancelButtonLabel = __( 'Cancel' ), localStorageKey, + type, } = options; return async ( onConfirm: () => void, { detail: detailOverride }: { detail?: string } = {} ) => { - if ( localStorage.getItem( localStorageKey ) === 'true' ) { + if ( localStorageKey && localStorage.getItem( localStorageKey ) === 'true' ) { onConfirm(); return; } @@ -30,16 +32,19 @@ export function useConfirmationDialog( options: ConfirmationDialogOptions ) { const CONFIRM_BUTTON_INDEX = 0; const CANCEL_BUTTON_INDEX = 1; const { response, checkboxChecked } = await getIpcApi().showMessageBox( { + type, message, detail: detailOverride ?? detail, - checkboxLabel, + ...( localStorageKey && { + checkboxLabel: checkboxLabel ?? __( "Don't show this warning again" ), + } ), buttons: [ confirmButtonLabel, cancelButtonLabel ], cancelId: CANCEL_BUTTON_INDEX, } ); if ( response === CONFIRM_BUTTON_INDEX ) { // Confirm button is always the first button - if ( checkboxChecked ) { + if ( localStorageKey && checkboxChecked ) { localStorage.setItem( localStorageKey, 'true' ); } onConfirm(); diff --git a/src/hooks/use-content-tabs.tsx b/src/hooks/use-content-tabs.tsx index 945ea365a..0fdab2136 100644 --- a/src/hooks/use-content-tabs.tsx +++ b/src/hooks/use-content-tabs.tsx @@ -1,14 +1,23 @@ import { TabPanel } from '@wordpress/components'; import { useI18n } from '@wordpress/react-i18n'; import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; +import { useFeatureFlags } from './use-feature-flags'; -export type TabName = 'overview' | 'share' | 'sync' | 'settings' | 'assistant' | 'import-export'; +export type TabName = + | 'overview' + | 'share' + | 'sync' + | 'settings' + | 'assistant' + | 'import-export' + | 'previews'; type Tab = React.ComponentProps< typeof TabPanel >[ 'tabs' ][ number ] & { name: TabName; }; function useTabs() { const { __ } = useI18n(); + const { quickDeploysEnabled } = useFeatureFlags(); return useMemo( () => { const tabs: Tab[] = [ @@ -22,11 +31,23 @@ function useTabs() { name: 'sync', title: __( 'Sync' ), }, - { + ]; + + if ( quickDeploysEnabled ) { + tabs.push( { + order: 3, + name: 'previews', + title: __( 'Previews' ), + } ); + } else { + tabs.push( { order: 3, name: 'share', title: __( 'Share' ), - }, + } ); + } + + tabs.push( { order: 4, name: 'import-export', @@ -36,8 +57,8 @@ function useTabs() { order: 5, name: 'settings', title: __( 'Settings' ), - }, - ]; + } + ); tabs.push( { order: 6, @@ -47,7 +68,7 @@ function useTabs() { } ); return tabs.sort( ( a, b ) => a.order - b.order ); - }, [ __ ] ); + }, [ __, quickDeploysEnabled ] ); } interface ContentTabsContextType { selectedTab: TabName; diff --git a/src/hooks/use-update-demo-site.tsx b/src/hooks/use-update-demo-site.tsx index 38c637b17..7563ea136 100644 --- a/src/hooks/use-update-demo-site.tsx +++ b/src/hooks/use-update-demo-site.tsx @@ -5,11 +5,12 @@ import { useCallback, useState, createContext, useContext, useMemo, ReactNode } import { DEMO_SITE_SIZE_LIMIT_BYTES, DEMO_SITE_SIZE_LIMIT_GB } from '../constants'; import { getIpcApi } from '../lib/get-ipc-api'; import { useAuth } from './use-auth'; +import { useFeatureFlags } from './use-feature-flags'; import { useSnapshots } from './use-snapshots'; interface DemoSiteUpdateContextType { updateDemoSite: ( snapshot: Snapshot, localSite: SiteDetails ) => Promise< void >; - isDemoSiteUpdating: ( siteId: string ) => boolean; + isDemoSiteUpdating: ( atomicSiteId: number ) => boolean; } const DemoSiteUpdateContext = createContext< DemoSiteUpdateContextType >( { @@ -24,8 +25,9 @@ interface DemoSiteUpdateProviderProps { export const DemoSiteUpdateProvider: React.FC< DemoSiteUpdateProviderProps > = ( { children } ) => { const { client } = useAuth(); const { __ } = useI18n(); - const [ updatingSites, setUpdatingSites ] = useState< Set< string > >( new Set() ); + const [ updatingSites, setUpdatingSites ] = useState< Set< number > >( new Set() ); const { updateSnapshot } = useSnapshots(); + const { quickDeploysEnabled } = useFeatureFlags(); const updateDemoSite = useCallback( async ( snapshot: Snapshot, localSite: SiteDetails ) => { @@ -33,7 +35,7 @@ export const DemoSiteUpdateProvider: React.FC< DemoSiteUpdateProviderProps > = ( // No-op if logged out return; } - setUpdatingSites( ( prev ) => new Set( prev ).add( localSite.id ) ); + setUpdatingSites( ( prev ) => new Set( prev ).add( snapshot.atomicSiteId ) ); let archivePath = ''; try { @@ -45,7 +47,9 @@ export const DemoSiteUpdateProvider: React.FC< DemoSiteUpdateProviderProps > = ( if ( archiveSizeInBytes > DEMO_SITE_SIZE_LIMIT_BYTES ) { getIpcApi().showErrorMessageBox( { - title: __( 'Updating demo site failed' ), + title: quickDeploysEnabled + ? __( 'Updating preview site failed' ) + : __( 'Updating demo site failed' ), message: sprintf( __( 'The site exceeds the maximum size of %dGB. Please remove some files and try again.' @@ -56,7 +60,7 @@ export const DemoSiteUpdateProvider: React.FC< DemoSiteUpdateProviderProps > = ( setUpdatingSites( ( prev ) => { const newSet = new Set( prev ); - newSet.delete( localSite.id ); + newSet.delete( snapshot.atomicSiteId ); return newSet; } ); getIpcApi().removeTemporalFile( archivePath ); @@ -92,35 +96,44 @@ export const DemoSiteUpdateProvider: React.FC< DemoSiteUpdateProviderProps > = ( } ); await getIpcApi().showNotification( { title: __( 'Update Successful' ), - body: sprintf( __( "Demo site for '%s' has been updated." ), localSite.name ), + body: quickDeploysEnabled + ? sprintf( __( "Preview site for '%s' has been updated." ), localSite.name ) + : sprintf( __( "Demo site for '%s' has been updated." ), localSite.name ), } ); return response; } catch ( error ) { getIpcApi().showErrorMessageBox( { title: __( 'Update failed' ), - message: sprintf( - __( "We couldn't update the %s demo site. Please try again." ), - localSite.name - ), + message: quickDeploysEnabled + ? sprintf( + __( "We couldn't update the %s preview site. Please try again." ), + localSite.name + ) + : sprintf( + __( "We couldn't update the %s demo site. Please try again." ), + localSite.name + ), error, } ); Sentry.captureException( error ); } finally { setUpdatingSites( ( prev ) => { - const newSet = new Set( prev ); - newSet.delete( localSite.id ); - return newSet; + const next = new Set( prev ); + next.delete( snapshot.atomicSiteId ); + return next; } ); if ( archivePath ) { getIpcApi().removeTemporalFile( archivePath ); } } }, - [ __, client, updateSnapshot ] + [ __, client, updateSnapshot, quickDeploysEnabled ] ); const isDemoSiteUpdating = useCallback( - ( siteId: string ) => updatingSites.has( siteId ), + ( atomicSiteId: number ) => { + return updatingSites.has( atomicSiteId ); + }, [ updatingSites ] ); diff --git a/src/modules/preview-site/components/create-preview-button.tsx b/src/modules/preview-site/components/create-preview-button.tsx new file mode 100644 index 000000000..b27174a5c --- /dev/null +++ b/src/modules/preview-site/components/create-preview-button.tsx @@ -0,0 +1,87 @@ +import { __, sprintf } from '@wordpress/i18n'; +import { useI18n } from '@wordpress/react-i18n'; +import Button from 'src/components/button'; +import offlineIcon from 'src/components/offline-icon'; +import { Tooltip } from 'src/components/tooltip'; +import { DEMO_SITE_SIZE_LIMIT_GB } from 'src/constants'; +import { useArchiveErrorMessages } from 'src/hooks/use-archive-error-messages'; +import { useArchiveSite } from 'src/hooks/use-archive-site'; +import { useOffline } from 'src/hooks/use-offline'; +import { useSiteSize } from 'src/hooks/use-site-size'; +import { useSnapshots } from 'src/hooks/use-snapshots'; + +interface CreatePreviewButtonProps { + onClick: () => void; + selectedSite: SiteDetails; +} + +export function CreatePreviewButton( { onClick, selectedSite }: CreatePreviewButtonProps ) { + const { __, _n } = useI18n(); + const { isAnySiteArchiving } = useArchiveSite(); + const { activeSnapshotCount, snapshotQuota, isLoadingSnapshotUsage, snapshotCreationBlocked } = + useSnapshots(); + const isLimitUsed = activeSnapshotCount >= snapshotQuota; + const { isOverLimit } = useSiteSize( selectedSite.id ); + const isOffline = useOffline(); + const errorMessages = useArchiveErrorMessages(); + + const isDisabled = + isAnySiteArchiving || + isLoadingSnapshotUsage || + isLimitUsed || + isOffline || + snapshotCreationBlocked; + + const siteArchivingMessage = __( + 'A different preview site is being created. Please wait for it to finish before creating another.' + ); + const allotmentConsumptionMessage = sprintf( + _n( + "You've used %s preview sites available on your account.", + "You've used all %s preview sites available on your account.", + snapshotQuota + ), + snapshotQuota + ); + const offlineMessage = __( 'Creating a preview site requires an internet connection.' ); + const overLimitMessage = sprintf( + __( + 'Your site exceeds %s GB in size. Creating a preview site for a larger site may take considerable amount of time and could exceed the maximum allowed size for a preview site.' + ), + DEMO_SITE_SIZE_LIMIT_GB + ); + + let tooltipContent; + if ( isOffline ) { + tooltipContent = { + icon: offlineIcon, + text: offlineMessage, + }; + } else if ( isLimitUsed ) { + tooltipContent = { text: allotmentConsumptionMessage }; + } else if ( isAnySiteArchiving ) { + tooltipContent = { text: siteArchivingMessage }; + } else if ( snapshotCreationBlocked ) { + tooltipContent = { text: errorMessages.rest_site_creation_blocked }; + } else if ( isOverLimit ) { + tooltipContent = { text: overLimitMessage }; + } + + return ( + + + + ); +} diff --git a/src/modules/preview-site/components/preview-action-buttons-menu.tsx b/src/modules/preview-site/components/preview-action-buttons-menu.tsx new file mode 100644 index 000000000..356669432 --- /dev/null +++ b/src/modules/preview-site/components/preview-action-buttons-menu.tsx @@ -0,0 +1,84 @@ +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { moreVertical } from '@wordpress/icons'; +import { useI18n } from '@wordpress/react-i18n'; +import { useConfirmationDialog } from 'src/hooks/use-confirmation-dialog'; +import { useSnapshots } from 'src/hooks/use-snapshots'; +import { useUpdateDemoSite } from 'src/hooks/use-update-demo-site'; + +interface PreviewActionButtonsMenuProps { + snapshot: Snapshot; + selectedSite: SiteDetails; +} + +export function PreviewActionButtonsMenu( { + snapshot, + selectedSite, +}: PreviewActionButtonsMenuProps ) { + const { __ } = useI18n(); + const { deleteSnapshot } = useSnapshots(); + const { updateDemoSite } = useUpdateDemoSite(); + + const showUpdatePreviewConfirmation = useConfirmationDialog( { + localStorageKey: 'dontShowUpdateWarning', + message: __( 'Overwrite preview' ), + detail: __( + "Updating will replace the existing files and database with a copy from your local site. Any changes you've made to your preview site will be permanently lost." + ), + confirmButtonLabel: __( 'Update' ), + } ); + + const handleUpdatePreviewSite = async () => { + showUpdatePreviewConfirmation( () => { + updateDemoSite( snapshot, selectedSite ); + } ); + }; + + const showDeletePreviewConfirmation = useConfirmationDialog( { + type: 'warning', + message: __( 'Delete preview' ), + detail: __( + 'Your previews files and database along with all posts, pages, comments and media will be lost.' + ), + confirmButtonLabel: __( 'Delete' ), + } ); + + const handleDeletePreviewSite = async () => { + showDeletePreviewConfirmation( () => { + deleteSnapshot( snapshot ); + } ); + }; + + return ( + + { ( { onClose }: { onClose: () => void } ) => ( + + + { __( 'Rename' ) } + + { + handleUpdatePreviewSite(); + onClose(); + } } + > + { __( 'Update' ) } + + { + handleDeletePreviewSite(); + onClose(); + } } + > + { __( 'Delete' ) } + + + ) } + + ); +} diff --git a/src/modules/preview-site/components/preview-site-row.tsx b/src/modules/preview-site/components/preview-site-row.tsx new file mode 100644 index 000000000..70c2c4bec --- /dev/null +++ b/src/modules/preview-site/components/preview-site-row.tsx @@ -0,0 +1,86 @@ +import { Spinner } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useI18n } from '@wordpress/react-i18n'; +import { useEffect } from 'react'; +import { ArrowIcon } from 'src/components/arrow-icon'; +import Button from 'src/components/button'; +import { useExpirationDate } from 'src/hooks/use-expiration-date'; +import { useFormatLocalizedTimestamps } from 'src/hooks/use-format-localized-timestamps'; +import { useSnapshots } from 'src/hooks/use-snapshots'; +import { useUpdateDemoSite } from 'src/hooks/use-update-demo-site'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import { PreviewActionButtonsMenu } from './preview-action-buttons-menu'; +import { ProgressRow } from './progress-row'; + +interface PreviewSiteRowProps { + snapshot: Snapshot; + selectedSite: SiteDetails; +} + +export function PreviewSiteRow( { snapshot, selectedSite }: PreviewSiteRowProps ) { + const { __ } = useI18n(); + const { url, date, isDeleting } = snapshot; + const { countDown } = useExpirationDate( date ); + const { fetchSnapshotUsage } = useSnapshots(); + const { isDemoSiteUpdating } = useUpdateDemoSite(); + const isPreviewSiteUpdating = isDemoSiteUpdating( snapshot.atomicSiteId ); + const { formatRelativeTime } = useFormatLocalizedTimestamps(); + + const getLastUpdateTimeText = () => { + if ( ! date ) { + return '-'; + } + const timeDistance = formatRelativeTime( new Date( date ).toISOString() ); + return sprintf( __( '%s ago' ), timeDistance ); + }; + + useEffect( () => { + fetchSnapshotUsage(); + }, [ fetchSnapshotUsage ] ); + + const urlWithHTTPS = `https://${ url }`; + + if ( isDeleting ) { + return ; + } + + return ( +
+
+
+
+
+ { selectedSite.name } +
+
+ +
+
+
+ { isPreviewSiteUpdating ? ( +
+ + { __( 'Updating' ) } +
+ ) : ( + getLastUpdateTimeText() + ) } +
+
{ countDown }
+
+ +
+
+
+
+ ); +} diff --git a/src/modules/preview-site/components/preview-sites-table-header.tsx b/src/modules/preview-site/components/preview-sites-table-header.tsx new file mode 100644 index 000000000..3ba8efca7 --- /dev/null +++ b/src/modules/preview-site/components/preview-sites-table-header.tsx @@ -0,0 +1,18 @@ +import { __ } from '@wordpress/i18n'; +import { useI18n } from '@wordpress/react-i18n'; + +export function PreviewSitesTableHeader() { + const { __ } = useI18n(); + return ( +
+
+
{ __( 'Preview site' ) }
+
+
{ __( 'Updated' ) }
+
{ __( 'Expires' ) }
+
{ __( 'Actions' ) }
+
+
+
+ ); +} diff --git a/src/modules/preview-site/components/progress-row.tsx b/src/modules/preview-site/components/progress-row.tsx new file mode 100644 index 000000000..d36f02bde --- /dev/null +++ b/src/modules/preview-site/components/progress-row.tsx @@ -0,0 +1,34 @@ +import { __ } from '@wordpress/i18n'; +import ProgressBar from 'src/components/progress-bar'; +import { useProgressTimer } from 'src/hooks/use-progress-timer'; + +interface ProgressRowProps { + text: string; +} + +export function ProgressRow( { text }: ProgressRowProps ) { + const { progress } = useProgressTimer( { + initialProgress: 20, + paused: false, + interval: 300, + maxValue: 95, + } ); + + return ( +
+
+
+
+
{ text }
+ +
+
+
+
{ '-' }
+
{ '-' }
+
+
+
+
+ ); +}