-
Notifications
You must be signed in to change notification settings - Fork 2k
Dashboard v2: Add the ability to configure sftp and ssh #103783
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
base: trunk
Are you sure you want to change the base?
Conversation
Jetpack Cloud live (direct link)
Automattic for Agencies live (direct link)
|
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser: App Entrypoints (~903 bytes added 📈 [gzipped])
Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used. Sections (~1310 bytes added 📈 [gzipped])
Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to. Async-loaded Components (~44 bytes added 📈 [gzipped])
React components that are loaded lazily, when a certain part of UI is displayed for the first time. Legend What is parsed and gzip size?Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory. Generated by performance advisor bot at iscalypsofastyet.com. |
372107a
to
832bb2c
Compare
832bb2c
to
a328796
Compare
import { copySmall } from '@wordpress/icons'; | ||
import React, { useState, useEffect } from 'react'; | ||
|
||
export default function ClipboardInputControl( |
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.
Do we need to move it to @automattic/dataviews
or @automattic/components
?
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.
Maybe we should wait for a bit before we propose lifting it up into a package. See if it changes much. The "Copied" message for example seems like it might be hard to standardise? The DS might not know the best way for your app to show success messages?
</Text> | ||
</VStack> | ||
<VStack spacing={ 4 } style={ { padding: '8px 0' } }> | ||
<ClipboardInputControl |
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.
Do we need to apply DataForm here 🤔
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 know they are readonly, but I think we should. We want these controls to have a style consistent with the rest of the dashboard, and using DataForm
seems like the correct way to do this. Even if it is over powered.
) } | ||
</Text> | ||
</VStack> | ||
<VStack spacing={ 4 } style={ { padding: '8px 0' } }> |
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.
There are 3 actions here
- toggle the ssh access
- attach ssh key
- detach ssh key
Do we need to apply DataForm here 🤔
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.
Personally I think it makes sense to use a DataForm
here too. Just IMO. It's the best way to keep all the forms looking and working the same. E.g. errors can be returned by these 3 actions, and we want to the error messages to be implemented the same way.
Perhaps the attach button should be the one with the type="submit"
since it's the one that actually makes use of one of the form fields?
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.
Hmmm...but how about the Upsell? I think the callout needs to mention both sftp and ssh. Also, how can people know they need to upgrade to have ssh from the Pro plan (is it still available?)? It seems we still need another upsell if the ssh is not available 🤔 |
Created. See DOTCOM-13410. |
<Button | ||
size="small" | ||
icon={ copySmall } | ||
label={ |
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.
It doesn't seem to have a way to display the tooltip immediately after the click...
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.
Could we show a snackbar notification? It's different from how Calypso does it, but seems to fit in with the design system.
I don't think changing the button's label to show the "Copied" message is correct for a11y.
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.
Yes, changing the aria-label like this is good for accessibility, as long as you manage the timing and state properly. It gives meaningful, real-time feedback to users relying on assistive technology.
I asked ChatGPT, and it seems fine 🙂 I also noticed that we’ve done the same thing in several other places so it should be okay.
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.
Show the snackbar by 160b813
Nice one, gave this a quick shot: Looks like you fixed the heading size? Looks more or less right to me. Separately (i.e. not for this PR), the button to link to "upgrade plan", instead of putting a premium plan in your cart and taking you to checkout, can we link to a plans page? I'm imagining something like https://wordpress.com/plans/, but without a need to select a site because you've already drilled into a particular site when you click this button, so we know which one to pick for you. CC: @matt-west Motivation: as a user I'd like to pore over which features I get and how I can get the best deal. Just going to the checkout cart I'm not sure what I'm getting. |
Oh, curious note. If you navigate from settings overview to Settings → SFTP, the heading font size is correct. But if when you're on that page, you reload, the heading font size is small: The font size is that of an H6. Some issues with that, firstly that this should not be an H6, because the only other heading on that page says "SFTP/SSH", and is an H1. If we need for it to be a heading, it should be an H2, the next in line from H1. Because this is using the "Callout" reusable component (kudos), this to me says we should not use a heading at all for that element. It can be a paragraph of bold text. That way it can work in any context without worrying about the accessibility of a heading hierarchy. On the flipside, if we DO want that to be a heading, then we need to either have the heading level smartly set by the system to always be 1 lower than the preceeding heading, OR have a prop so the implementer can choose the heading level. Finally there's the font size itself. It is designed to be a "LG Heading" (i.e. 15px and medium font weight). Whether a paragraph or a heading, that element should always have that visual style, and it should not be subject to an otherwise type hierarchy. This is both a comment on this CSS: I don't think any of the headings should ever be smaller than the 13px font size. If this needs a separate issue, worth opening one at the DS level. Separately, it could be an argument for having heading level CSS classes. WordPress.org does this in a reflection that the semantic heading hierarchy doesn't always match the visual hierarchy. For example theme card headings are H2s and have these CSS classes:
Finally, there's a double border at the bottom of all summary groups: ![]() In fact this appears to be coming from an empty list item in every one of those groups: ![]() |
cc @p-jackson What do you think 🤔 |
Looks like that is going to be fixed by #103876
Fwiw the callout component does have a I implemented However the site settings are different. The page content can't be inert because the user needs access to the breadcrumb. In this case the callout title shouldn't be a heading, the page already has an We did discuss the correct way to implement the title prop in the original |
Created a follow up: DOTCOM-13434 |
return null; | ||
} | ||
|
||
if ( ! canUseSftp( site ) ) { |
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.
This logic doesn't match the callout copy since simple sites with a Business Plan also match ! canUseSftp()
.
I shared thoughts in this discussion about why I don't think we should reuse the callout pattern in this situation QOBjujZah0UlGDDdLKZhzi-fi-563_32922#1245696310.
I don't think we have any UI for initiating atomic transfer yet? I personally think it'd be ok for this PR if it rendered some TODO text saying you need to transfer to atomic.
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.
Oh, I see now that all of our atomic settings share this same issue 😃. I suppose it can be ignored for this PR then.
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.
Yup exactly. I already created an issue for this 🙈 DOTCOM-13376
<Button | ||
size="small" | ||
icon={ copySmall } | ||
label={ |
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.
Could we show a snackbar notification? It's different from how Calypso does it, but seems to fit in with the design system.
I don't think changing the button's label to show the "Copied" message is correct for a11y.
import { copySmall } from '@wordpress/icons'; | ||
import React, { useState, useEffect } from 'react'; | ||
|
||
export default function ClipboardInputControl( |
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.
Maybe we should wait for a bit before we propose lifting it up into a package. See if it changes much. The "Copied" message for example seems like it might be hard to standardise? The DS might not know the best way for your app to show success messages?
@@ -177,6 +190,18 @@ export function profileMutation() { | |||
}; | |||
} | |||
|
|||
export function profileSshKeysQuery() { | |||
return { | |||
queryKey: [ 'profile', 'ssh-keys' ], |
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.
Should this query key have two parts? Or should it just be [ 'profile-ssh-keys' ]
. I'm not sure, just trying to think it through. By giving it two parts it means that if you ever cleared the user profile data with queryClient.invalidateQueries( { queryKey: [ 'profile' ] } );
then it would also clear out this ssh key data too. Does that seem right? Maybe it is 🤷♂️
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.
Yes, that's correct. However, I assume that when the profile data changes, the profile-related data is expected to be invalidated. Does that make sense?
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.
Perhaps it should be 🤔
client/dashboard/app/router.tsx
Outdated
if ( canUseSftp( site ) ) { | ||
await queryClient.ensureQueryData( siteSftpUsersQuery( siteSlug ) ); | ||
} | ||
|
||
if ( canUseSsh( site ) ) { | ||
await queryClient.ensureQueryData( siteSshAccessStatusQuery( siteSlug ) ); | ||
} |
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.
Promise.all()
doesn't mind if you pass it a boolean (it's as if its passed an already resolved boolean promise), so you can still run the queries in parallel like this.
if ( canUseSftp( site ) ) { | |
await queryClient.ensureQueryData( siteSftpUsersQuery( siteSlug ) ); | |
} | |
if ( canUseSsh( site ) ) { | |
await queryClient.ensureQueryData( siteSshAccessStatusQuery( siteSlug ) ); | |
} | |
return Promise.all( [ | |
canUseSftp( site ) && queryClient.ensureQueryData( siteSftpUsersQuery( siteSlug ) ), | |
canUseSsh( site ) && queryClient.ensureQueryData( siteSshAccessStatusQuery( siteSlug ) ), | |
] ); |
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.
Done by 276ce4c.
</Text> | ||
</VStack> | ||
<VStack spacing={ 4 } style={ { padding: '8px 0' } }> | ||
<ClipboardInputControl |
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 know they are readonly, but I think we should. We want these controls to have a style consistent with the rest of the dashboard, and using DataForm
seems like the correct way to do this. Even if it is over powered.
) } | ||
</Text> | ||
</VStack> | ||
<VStack spacing={ 4 } style={ { padding: '8px 0' } }> |
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.
Personally I think it makes sense to use a DataForm
here too. Just IMO. It's the best way to keep all the forms looking and working the same. E.g. errors can be returned by these 3 actions, and we want to the error messages to be implemented the same way.
Perhaps the attach button should be the one with the type="submit"
since it's the one that actually makes use of one of the form fields?
<Button | ||
variant="secondary" | ||
isBusy={ mutation.isPending } | ||
onClick={ handleCreatePassword } | ||
> | ||
{ __( 'Reset password' ) } | ||
</Button> |
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.
Maybe it's just me but I think this should have isDestructive
because it's resetting your existing password without any confirmation
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 think it’s fine because it’s not a scary button, and nothing will be broken or removed. People can feel free to reset their password if they forget it.
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.
Resetting passwords can be a big deal though. The fact we only support a single FTP password is a limitation. The user might have saved the password into a GitHub Action environment variable. Deleting the password will break their CI pipeline.
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.
+1. We should make it scary in order for users to take a step back and think hard what resetting the password would entail.
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.
Many ways to scarify, by the way! dhW7zYBamXJIeJIE3wy5f4-fi-3074_108567
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.
We have some guidelines around usage of isDestructive
in the design system docs and it's mostly reserved for big actions like deleting an entire site. I don't think it's necessary here.
If we’re concerned that people will reset their password and break other apps that are relying on that password we should present a confirmation dialog instead.
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 see, thanks! Let's not change anything I think for now 😄
icon={ trash } | ||
label={ __( 'Detach' ) } | ||
isBusy={ isBusy } | ||
style={ { margin: '-6px' } } |
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.
Would it make more sense to simply reduce the HStack
's spacing?
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 don't think so. The reason for the negative margin is because the button has padding to increase the selection area (I’m guessing?), but that caused it to be misaligned with the layout. Maybe the margin should be applied inside the button instead, if that’s the reason 🤔
return ( | ||
<RouterLinkSummaryButton | ||
to={ `/sites/${ site.slug }/settings/sftp-ssh` } | ||
title="SFTP/SSH" |
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.
We're translating the settings title but not here. Let's be consistent :)
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.
Updated by ee9d5cd.
<RouterLinkSummaryButton | ||
to={ `/sites/${ site.slug }/settings/sftp-ssh` } | ||
title="SFTP/SSH" | ||
density="medium" |
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.
We now need to pass density
from the parent SummaryButtonList
; please check the other setting summaries on trunk
.
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.
Updated by 809b42c.
return ( | ||
<Card> | ||
<CardBody> | ||
<VStack style={ { paddingBottom: '12px' } }> |
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.
Seems like we should standardize card's heading and description into a component, otherwise we will keep inventing custom padding, margin, text sizes, ... 😄 (just rant, not really a comment)
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.
Totally agreed 😭
I changed to use DataForm by 3726113 and ceeef5b. However, I didn’t set the button type to submit, since both forms have optional buttons, and it feels a bit odd to trigger a submit behavior when the buttons might not even be present 🤔 |
This PR modifies the release build for the following Calypso Apps: For info about this notification, see here: PCYsg-OT6-p2
To test WordPress.com changes, run |
Thanks for the context! I've no strong opinion on whether it's a heading or bold text, just noting that if it is a heading, we need CSS that sets the visual appearance to always be the same regardless of what styles (notably font sizes and weight) might be inherited from the default h1-6 definitions. |
Agreed, although perhaps this is something we should test in the future. I think we already have the ability to show a condensed version of the plans grid that only shows a sub-set of plans. |
import { copySmall } from '@wordpress/icons'; | ||
import React, { useState, useEffect } from 'react'; | ||
|
||
export default function ClipboardInputControl( { |
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.
Can we move this to client/dashboard/components
? I will need it for the Coming Soon's "share site" feature 🙈
|
||
return ( | ||
<PageLayout size="small" header={ <SettingsPageHeader title={ __( 'SFTP/SSH' ) } /> }> | ||
<VStack spacing={ 8 }> |
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 believe PageLayout
already has <VStack spacing={8}>
for the children, can we check 🤔
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.
Oh you're right!
label={ field.label } | ||
value={ value } | ||
options={ field.elements ?? [] } | ||
onChange={ ( value ) => onChange( { [ id ]: value } ) } |
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 don't think onChange
is defined here?
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 think we can just use the newly created
return <RequiredSelect {...props} />;
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.
Thanks for working on this @arthur791004. I left some comments.
I’m aware that DOTCOM-13376 also impacts the SFTP/SSH section. I’ll follow-up with a design for that ASAP.
: sprintf( | ||
/* translators: %s is the field to copy */ | ||
__( 'Copy %s' ), | ||
props.label |
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.
Is it possible for use to downcast the field name?
At the moment we show Copy Username
, but to be consistent with our copy guidelines this should really be sentence case, so Copy username
. We’d need to watch out for cases where the field name is an acronym though, like Copy URL
which should remain all-caps.
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.
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.
Let me think about how to do it.
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.
Maybe for this first iteration, if it's too convoluted, we can just show Copied!
?
(Trying to unblock things here)
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.
Copied!
is a bit too generic for the snackbar. If you copy a few different fields in quick successions, you'll end up with a bunch of identical snackbars.
return ( | ||
<Card> | ||
<CardBody> | ||
<Text variant="muted" lineHeight="20px" style={ { paddingBottom: '12px' } } as="p"> |
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.
onError: () => { | ||
createErrorNotice( | ||
__( | ||
'Sorry, we had a problem retrieving your sftp user details. Please refresh the page and try again.' |
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.
SFTP should be all-caps as it's an acronym.
createSuccessNotice( | ||
sprintf( | ||
/* translators: %s is the copied field */ | ||
__( 'Copied %s to clipboard.' ), |
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.
This should be sentence case if possible. At the moment it can appear as Copied Connection command to clipboard.
createSuccessNotice( | ||
sprintf( | ||
/* translators: %s is the copied field */ | ||
__( 'Copied %s to clipboard.' ), |
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.
Ditto comment about sentence casing the string so this becomes:
Copied connection command to clipboard.
); | ||
}; | ||
|
||
const handleToggleSshAccess = () => { |
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.
We should show a Snackbar when SSH is enabled/disabled.
/> | ||
) } | ||
</VStack> | ||
{ showSshKeysSelect && ( |
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.
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.
Nice catch!
onDetach: ( siteSshKey: SiteSshKey ) => void; | ||
} ) => { | ||
return ( | ||
<Card> |
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.
The padding and border-radius are off on this card.

We might be able to fix the padding issue by setting the size
prop to small
.
The border-radius is trickier as the component only appears to support a single isRounded
prop. We typically reduce the border-radius of elements that are nested within a card. In this case we should use radius-s
(2px) so the SSH key card matches the radius used on form inputs. If the only way to achieve this is a hacky CSS override, then perhaps we skip it. It would be good for the Card component to support this flexibility though.
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 think the CSS is the only way 🥲
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.
Okay, let's leave the radius as-is then. I'd rather not introduce tech debt.
|
||
const badges = [ | ||
{ | ||
text: sftpEnabled ? __( 'SFTP enabled' ) : __( 'SFTP disabled' ), |
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 don't think badges for disabled states are strictly needed in the settings list. They can add a lot of unnecessary clutter to the settings list.

This feels like something we should agree on at the design system-level though. @jasmussen, @jameskoster, do you have an opinion on whether to display badges for disabled states in SummaryButtons?
Part of DOTCOM-13254
Proposed Changes
Use InputControlSuffixWrapper to add "copy" iconUse DataForm - Do we need to use it for the readonly form?Done by by 3726113 and ceeef5bSite Settings
Callout
The current styles of title is weird...

Create credentials (Need to confirm the UI)
SFTP Enabled
SSH Enabled
SSH Key Attached
Why are these changes being made?
Testing Instructions
Pre-merge Checklist