Skip to content

Commit 87f8f63

Browse files
authored
Dashboard v2: Add the ability to configure sftp and ssh (#103783)
* Dashboard v2: Add sftp/ssh data and queries * Dashboard v2: Add EnableSftpCard * Dashboard v2: Add SftpCard * Dashboard v2: Add SshCard * Dashboard v2: Display sftp callout * Make it work * Dashboard v2: Implement ClipboardInputControl * Add label to trash icon and use isBusy instead of disabled * Fix types * Update EnableSftpCard stypes * Update SftpCard stypes * Update SshCard styles * Move permission check to site-features * Address feedback * Show snackbar when the value is copied * Update density of the summary button * Display error notices * Use DataForm on SftpCard * Use DataForm on SshCard * Translate SFTP/SSH * Use RequiredSelect * Remove VStack inside PageLayout * Move ClipboardInputControl to dashboard/components * Add sucess notice when enabling/disabling ssh access * Use lowercase for label * Use SectionHeader * Fix copy * Change the size of SshKeyCard to small * Handle empty user ssh keys * Handle reauthorization_required * Show confirm dialog for resetting password * Fix types * Use wpcom.req.post for delete method * Fix the default value of the ssh_key * Fix lint * Fix notice
1 parent 20de145 commit 87f8f63

File tree

15 files changed

+1204
-3
lines changed

15 files changed

+1204
-3
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ module.exports = {
503503
'@wordpress/date': [ '__experimentalGetSettings' ],
504504
'@wordpress/edit-post': [ '__experimentalMainDashboardButton' ],
505505
'@wordpress/components': [
506+
'__experimentalConfirmDialog',
506507
'__experimentalDivider',
507508
'__experimentalHStack',
508509
'__experimentalVStack',

client/dashboard/app/queries.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ import {
4040
resetSite,
4141
fetchSiteResetStatus,
4242
launchSite,
43+
fetchSftpUsers,
44+
createSftpUser,
45+
resetSftpPassword,
46+
fetchSshAccessStatus,
47+
enableSshAccess,
48+
disableSshAccess,
49+
fetchSiteSshKeys,
50+
fetchProfileSshKeys,
51+
attachSiteSshKey,
52+
detachSiteSshKey,
4353
} from '../data';
4454
import { SITE_FIELDS, SITE_OPTIONS } from '../data/constants';
4555
import { queryClient } from './query-client';
@@ -51,6 +61,9 @@ import type {
5161
DefensiveModeSettings,
5262
DefensiveModeSettingsUpdate,
5363
SiteTransferConfirmation,
64+
SshAccessStatus,
65+
SftpUser,
66+
SiteSshKey,
5467
} from '../data/types';
5568
import type { Query } from '@tanstack/react-query';
5669

@@ -182,6 +195,19 @@ export function profileMutation() {
182195
};
183196
}
184197

198+
export function profileSshKeysQuery() {
199+
return {
200+
queryKey: [ 'profile', 'ssh-keys' ],
201+
queryFn: () => {
202+
return fetchProfileSshKeys();
203+
},
204+
retry: false, // Don't retry on 401 errors
205+
meta: {
206+
persist: false,
207+
},
208+
};
209+
}
210+
185211
export function siteSettingsQuery( siteId: string ) {
186212
return {
187213
queryKey: [ 'site-settings', siteId ],
@@ -409,3 +435,107 @@ export function launchSiteMutation( siteIdOrSlug: string ) {
409435
},
410436
};
411437
}
438+
439+
export function siteSftpUsersQuery( siteId: string ) {
440+
return {
441+
queryKey: [ 'site', siteId, 'sftp-users' ],
442+
queryFn: () => {
443+
return fetchSftpUsers( siteId );
444+
},
445+
meta: {
446+
persist: false,
447+
},
448+
};
449+
}
450+
451+
const updateCurrentSftpUsers = ( currentSftpUsers: SftpUser[], sftpUser: SftpUser ) => {
452+
const index = currentSftpUsers.findIndex(
453+
( currentSftpUser ) => currentSftpUser.username === sftpUser.username
454+
);
455+
if ( index >= 0 ) {
456+
return [ ...currentSftpUsers.slice( 0, index ), sftpUser, ...currentSftpUsers.slice( 0 + 1 ) ];
457+
}
458+
459+
return [ ...currentSftpUsers, sftpUser ];
460+
};
461+
462+
export function siteSftpUsersCreateMutation( siteId: string ) {
463+
return {
464+
mutationFn: () => createSftpUser( siteId ),
465+
onSuccess: ( createdSftpUser: SftpUser ) => {
466+
queryClient.setQueryData(
467+
[ 'site', siteId, 'sftp-users' ],
468+
( currentSftpUsers: SftpUser[] ) =>
469+
updateCurrentSftpUsers( currentSftpUsers, createdSftpUser )
470+
);
471+
},
472+
};
473+
}
474+
475+
export function siteSftpUsersResetPasswordMutation( siteId: string ) {
476+
return {
477+
mutationFn: ( sshUsername: string ) => resetSftpPassword( siteId, sshUsername ),
478+
onSuccess: ( updatedSftpUser: SftpUser ) => {
479+
queryClient.setQueryData(
480+
[ 'site', siteId, 'sftp-users' ],
481+
( currentSftpUsers: SftpUser[] ) =>
482+
updateCurrentSftpUsers( currentSftpUsers, updatedSftpUser )
483+
);
484+
},
485+
};
486+
}
487+
488+
export function siteSshAccessStatusQuery( siteId: string ) {
489+
return {
490+
queryKey: [ 'site', siteId, 'ssh-access' ],
491+
queryFn: () => {
492+
return fetchSshAccessStatus( siteId );
493+
},
494+
};
495+
}
496+
497+
export function siteSshAccessEnableMutation( siteId: string ) {
498+
return {
499+
mutationFn: () => enableSshAccess( siteId ),
500+
onSuccess: ( data: SshAccessStatus ) => {
501+
queryClient.setQueryData( [ 'site', siteId, 'ssh-access' ], data );
502+
},
503+
};
504+
}
505+
506+
export function siteSshAccessDisableMutation( siteId: string ) {
507+
return {
508+
mutationFn: () => disableSshAccess( siteId ),
509+
onSuccess: ( data: SshAccessStatus ) => {
510+
queryClient.setQueryData( [ 'site', siteId, 'ssh-access' ], data );
511+
},
512+
};
513+
}
514+
515+
export function siteSshKeysQuery( siteId: string ) {
516+
return {
517+
queryKey: [ 'site', siteId, 'ssh-keys' ],
518+
queryFn: () => {
519+
return fetchSiteSshKeys( siteId );
520+
},
521+
};
522+
}
523+
524+
export function siteSshKeysAttachMutation( siteId: string ) {
525+
return {
526+
mutationFn: ( name: string ) => attachSiteSshKey( siteId, name ),
527+
onSuccess: () => {
528+
queryClient.invalidateQueries( { queryKey: [ 'site', siteId, 'ssh-keys' ] } );
529+
},
530+
};
531+
}
532+
533+
export function siteSshKeysDetachMutation( siteId: string ) {
534+
return {
535+
mutationFn: ( siteSshKey: SiteSshKey ) =>
536+
detachSiteSshKey( siteId, siteSshKey.user_login, siteSshKey.name ),
537+
onSuccess: () => {
538+
queryClient.invalidateQueries( { queryKey: [ 'site', siteId, 'ssh-keys' ] } );
539+
},
540+
};
541+
}

client/dashboard/app/router.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
canGetPrimaryDataCenter,
1515
canSetStaticFile404Handling,
1616
canUpdateCaching,
17+
canUseSftp,
18+
canUseSsh,
1719
} from '../utils/site-features';
1820
import NotFound from './404';
1921
import UnknownError from './500';
@@ -33,6 +35,8 @@ import {
3335
siteEdgeCacheStatusQuery,
3436
siteDefensiveModeQuery,
3537
agencyBlogQuery,
38+
siteSftpUsersQuery,
39+
siteSshAccessStatusQuery,
3640
} from './queries';
3741
import { queryClient } from './query-client';
3842
import Root from './root';
@@ -315,6 +319,24 @@ const siteSettingsDefensiveModeRoute = createRoute( {
315319
)
316320
);
317321

322+
const siteSettingsSftpSshRoute = createRoute( {
323+
getParentRoute: () => siteRoute,
324+
path: 'settings/sftp-ssh',
325+
loader: async ( { params: { siteSlug } } ) => {
326+
const site = await queryClient.ensureQueryData( siteQuery( siteSlug ) );
327+
return Promise.all( [
328+
canUseSftp( site ) && queryClient.ensureQueryData( siteSftpUsersQuery( siteSlug ) ),
329+
canUseSsh( site ) && queryClient.ensureQueryData( siteSshAccessStatusQuery( siteSlug ) ),
330+
] );
331+
},
332+
} ).lazy( () =>
333+
import( '../sites/settings-sftp-ssh' ).then( ( d ) =>
334+
createLazyRoute( 'site-settings-sftp-ssh' )( {
335+
component: () => <d.default siteSlug={ siteRoute.useParams().siteSlug } />,
336+
} )
337+
)
338+
);
339+
318340
const siteSettingsTransferSiteRoute = createRoute( {
319341
getParentRoute: () => siteRoute,
320342
path: 'settings/transfer-site',
@@ -501,6 +523,7 @@ const createRouteTree = ( config: AppConfig ) => {
501523
siteSettingsCachingRoute,
502524
siteSettingsDefensiveModeRoute,
503525
siteSettingsTransferSiteRoute,
526+
siteSettingsSftpSshRoute,
504527
] )
505528
);
506529
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
// eslint-disable-next-line wpcalypso/no-unsafe-wp-apis
3+
__experimentalInputControl as InputControl,
4+
// eslint-disable-next-line wpcalypso/no-unsafe-wp-apis
5+
__experimentalInputControlSuffixWrapper as InputControlSuffixWrapper,
6+
Button,
7+
} from '@wordpress/components';
8+
import { sprintf, __ } from '@wordpress/i18n';
9+
import { copySmall } from '@wordpress/icons';
10+
import React, { useState, useEffect } from 'react';
11+
12+
export default function ClipboardInputControl( {
13+
onCopy,
14+
...props
15+
}: Omit< React.ComponentProps< typeof InputControl >, 'onCopy' > & {
16+
onCopy?: ( label?: React.ReactNode ) => void;
17+
} ) {
18+
const [ isCopied, setCopied ] = useState( false );
19+
20+
const handleCopy = () => {
21+
if ( props.value ) {
22+
navigator.clipboard.writeText( props.value );
23+
setCopied( true );
24+
onCopy?.( props.label );
25+
}
26+
};
27+
28+
// toggle the `isCopied` flag back to `false` after 4 seconds
29+
useEffect( () => {
30+
if ( isCopied ) {
31+
const timerId = window.setTimeout( () => setCopied( false ), 4000 );
32+
return () => window.clearTimeout( timerId );
33+
}
34+
}, [ isCopied ] );
35+
36+
return (
37+
<InputControl
38+
{ ...props }
39+
suffix={
40+
<InputControlSuffixWrapper variant="control">
41+
<Button
42+
size="small"
43+
icon={ copySmall }
44+
label={
45+
isCopied
46+
? __( 'Copied' )
47+
: sprintf(
48+
/* translators: %s is the field to copy */
49+
__( 'Copy %s' ),
50+
props.label
51+
)
52+
}
53+
onClick={ handleCopy }
54+
/>
55+
</InputControlSuffixWrapper>
56+
}
57+
/>
58+
);
59+
}

client/dashboard/data/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export enum DotcomFeatures {
33
COPY_SITE = 'copy-site',
44
LEGACY_CONTACT = 'legacy-contact',
55
LOCKED_MODE = 'locked-mode',
6+
SFTP = 'sftp',
7+
SSH = 'ssh',
68
}
79

810
export const SITE_FIELDS = [

0 commit comments

Comments
 (0)