Skip to content

Commit 3bcc694

Browse files
authored
Settings improvements (#10924)
- Address enso-org/cloud-v2#1457 - Show x instead of a circle for close tab icon on Windows and Linux - Free / Solo no longer shows Organization, Members, User Groups, and Activity Log - Invite button should only be there for the admin in Team or above (should already be present) - Should list the required symbols in the password change. - Changing headshot appeared briefly and then disappeared. - Fix enso-org/cloud-v2#1455 - The `Alert` component no longer focuses itself on every keystroke. - Fix #10988 - Same issue as above. Other related changes: - Fix word wrapping in tooltips when a word is wider than the entire tooltip - Add word wrapping to "members" table in members tab - Limit length of "email" column of members table in "user groups" tab - Hide empty sections - Add text to empty "user groups" table in "user groups" tab - ~~Close "set organization name" modal when organization name is set~~ - Already fixed by another PR # Important Notes None
1 parent c87053d commit 3bcc694

File tree

18 files changed

+209
-129
lines changed

18 files changed

+209
-129
lines changed

app/.vscode/launch.json

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,80 @@
66
"request": "launch",
77
"name": "Dashboard",
88
"runtimeExecutable": "pnpm",
9-
"runtimeArgs": ["run", "--filter", "enso-dashboard", "dev"]
9+
"runtimeArgs": ["run", "--filter", "enso-dashboard", "dev"],
10+
"outputCapture": "std"
11+
},
12+
{
13+
"type": "node",
14+
"request": "launch",
15+
"name": "Dashboard (Electron, Linux)",
16+
"runtimeExecutable": "pnpm",
17+
"runtimeArgs": ["run", "--filter", "enso", "watch:linux"],
18+
"outputCapture": "std"
19+
},
20+
{
21+
"type": "node",
22+
"request": "launch",
23+
"name": "Dashboard (Electron, macOS)",
24+
"runtimeExecutable": "pnpm",
25+
"runtimeArgs": ["run", "--filter", "enso", "watch:macos"],
26+
"outputCapture": "std"
27+
},
28+
{
29+
"type": "node",
30+
"request": "launch",
31+
"name": "Dashboard (Electron, Windows)",
32+
"runtimeExecutable": "pnpm",
33+
"runtimeArgs": ["run", "--filter", "enso", "watch:windows"],
34+
"outputCapture": "std"
1035
},
1136
{
1237
"type": "node",
1338
"request": "launch",
1439
"name": "GUI",
1540
"runtimeExecutable": "pnpm",
16-
"runtimeArgs": ["dev:gui"]
41+
"runtimeArgs": ["dev:gui"],
42+
"outputCapture": "std"
1743
},
1844
{
1945
"type": "node",
2046
"request": "launch",
2147
"name": "GUI (Storybook)",
2248
"runtimeExecutable": "pnpm",
23-
"runtimeArgs": ["run", "--filter", "enso-gui2", "story:dev"]
49+
"runtimeArgs": ["run", "--filter", "enso-gui2", "story:dev"],
50+
"outputCapture": "std"
2451
},
2552
{
2653
"type": "node",
2754
"request": "launch",
2855
"name": "Dashboard (Build)",
2956
"runtimeExecutable": "pnpm",
30-
"runtimeArgs": ["build:gui"]
57+
"runtimeArgs": ["build:gui"],
58+
"outputCapture": "std"
3159
},
3260
{
3361
"type": "node",
3462
"request": "launch",
3563
"name": "Dashboard (E2E UI)",
3664
"runtimeExecutable": "pnpm",
37-
"runtimeArgs": ["run", "--filter", "enso-dashboard", "test:e2e:debug"]
65+
"runtimeArgs": ["run", "--filter", "enso-dashboard", "test:e2e:debug"],
66+
"outputCapture": "std"
3867
},
3968
{
4069
"type": "node",
4170
"request": "launch",
4271
"name": "GUI (E2E UI)",
4372
"runtimeExecutable": "pnpm",
44-
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e", "--", "--ui"]
73+
"runtimeArgs": ["run", "--filter", "enso-gui2", "test:e2e", "--", "--ui"],
74+
"outputCapture": "std"
4575
},
4676
{
4777
"type": "node",
4878
"request": "launch",
4979
"name": "Dashboard (All tests)",
5080
"runtimeExecutable": "pnpm",
51-
"runtimeArgs": ["run", "--filter", "enso-dashboard", "test"]
81+
"runtimeArgs": ["run", "--filter", "enso-dashboard", "test"],
82+
"outputCapture": "std"
5283
},
5384
{
5485
"type": "node",

app/dashboard/e2e/organizationSettings.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/** @file Test the organization settings tab. */
22
import * as test from '@playwright/test'
33

4+
import { Plan } from 'enso-common/src/services/Backend'
45
import * as actions from './actions'
56

67
test.test('organization settings', async ({ page }) => {
7-
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
8+
const api = await actions.mockAllAndLoginAndExposeAPI({
9+
page,
10+
setupAPI: (theApi) => {
11+
theApi.setPlan(Plan.team)
12+
},
13+
})
814
const localActions = actions.settings.organization
915

1016
// Setup
@@ -76,7 +82,12 @@ test.test('organization settings', async ({ page }) => {
7682
})
7783

7884
test.test('upload organization profile picture', async ({ page }) => {
79-
const api = await actions.mockAllAndLoginAndExposeAPI({ page })
85+
const api = await actions.mockAllAndLoginAndExposeAPI({
86+
page,
87+
setupAPI: (theApi) => {
88+
theApi.setPlan(Plan.team)
89+
},
90+
})
8091
const localActions = actions.settings.organizationProfilePicture
8192

8293
await localActions.go(page)

app/dashboard/src/components/AriaComponents/Alert/Alert.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** @file Alert component. */
22
import { type ForwardedRef, type HTMLAttributes, type PropsWithChildren } from 'react'
33

4-
import { mergeRefs } from '#/utilities/mergeRefs'
54
import { forwardRef } from '#/utilities/react'
65
import { tv, type VariantProps } from '#/utilities/tailwindVariants'
76

@@ -57,6 +56,7 @@ export interface AlertProps
5756
HTMLAttributes<HTMLDivElement> {}
5857

5958
/** Alert component. */
59+
// eslint-disable-next-line no-restricted-syntax
6060
export const Alert = forwardRef(function Alert(
6161
props: AlertProps,
6262
ref: ForwardedRef<HTMLDivElement>,
@@ -71,11 +71,7 @@ export const Alert = forwardRef(function Alert(
7171
return (
7272
<div
7373
className={ALERT_STYLES({ variant, size, className, rounded, fullWidth })}
74-
ref={mergeRefs(ref, (e) => {
75-
if (variant === 'error') {
76-
e?.focus()
77-
}
78-
})}
74+
ref={ref}
7975
{...containerProps}
8076
>
8177
{children}

app/dashboard/src/components/AriaComponents/Button/CloseButton.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,44 @@
11
/** @file A button for closing a modal. */
2-
3-
import * as React from 'react'
4-
5-
import Dismiss from '#/assets/dismiss.svg'
6-
7-
import * as textProvider from '#/providers/TextProvider'
8-
9-
import * as button from '#/components/AriaComponents/Button'
10-
11-
import * as tailwindMerge from '#/utilities/tailwindMerge'
2+
import DismissIcon from '#/assets/dismiss.svg'
3+
import { Button, type ButtonProps } from '#/components/AriaComponents/Button'
4+
import { useText } from '#/providers/TextProvider'
5+
import { twMerge } from '#/utilities/tailwindMerge'
6+
import { isOnMacOS } from 'enso-common/src/detect'
127

138
// ===================
149
// === CloseButton ===
1510
// ===================
1611

1712
/** Props for a {@link CloseButton}. */
18-
export type CloseButtonProps = Omit<
19-
button.ButtonProps,
20-
'children' | 'rounding' | 'size' | 'variant'
21-
>
13+
export type CloseButtonProps = Omit<ButtonProps, 'children' | 'rounding' | 'size' | 'variant'>
2214

2315
/** A styled button with a close icon that appears on hover. */
2416
export function CloseButton(props: CloseButtonProps) {
25-
const { getText } = textProvider.useText()
17+
const { getText } = useText()
2618
const {
2719
className,
28-
icon = Dismiss,
20+
icon = DismissIcon,
2921
tooltip = false,
3022
'aria-label': ariaLabel = getText('closeModalShortcut'),
3123
...buttonProps
3224
} = props
3325

3426
return (
35-
<button.Button
27+
<Button
3628
variant="icon"
3729
className={(values) =>
38-
tailwindMerge.twMerge(
39-
'bg-primary/30 hover:bg-red-500/80 focus-visible:bg-red-500/80 focus-visible:outline-offset-1',
30+
twMerge(
31+
'hover:bg-red-500/80 focus-visible:bg-red-500/80 focus-visible:outline-offset-1',
32+
isOnMacOS() ? 'bg-primary/30' : (
33+
'text-primary/90 hover:text-primary focus-visible:text-primary'
34+
),
4035
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
4136
// @ts-expect-error ts fails to infer the type of the className prop
4237
typeof className === 'function' ? className(values) : className,
4338
)
4439
}
4540
tooltip={tooltip}
46-
showIconOnHover
41+
showIconOnHover={isOnMacOS()}
4742
size="xsmall"
4843
rounded="full"
4944
extraClickZone="medium"

app/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import * as text from '../Text'
1212
// =================
1313

1414
export const TOOLTIP_STYLES = twv.tv({
15-
base: 'group flex justify-center items-center text-center text-balance break-all z-50',
15+
base: 'group flex justify-center items-center text-center text-balance [overflow-wrap:anywhere] z-tooltip',
1616
variants: {
1717
variant: {
1818
custom: '',

app/dashboard/src/components/Devtools/EnsoDevtools.tsx

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import * as reactQuery from '@tanstack/react-query'
99

1010
import { IS_DEV_MODE } from 'enso-common/src/detect'
1111

12-
import cross from '#/assets/cross.svg'
12+
import CrossIcon from '#/assets/cross.svg'
1313
import DevtoolsLogo from '#/assets/enso_logo.svg'
14-
import trash from '#/assets/trash.svg'
14+
import TrashIcon from '#/assets/trash.svg'
1515

1616
import { SETUP_PATH } from '#/appUtils'
1717

@@ -45,7 +45,7 @@ import {
4545
} from '#/providers/FeatureFlagsProvider'
4646
import { useLocalStorage } from '#/providers/LocalStorageProvider'
4747
import * as backend from '#/services/Backend'
48-
import LocalStorage from '#/utilities/LocalStorage'
48+
import LocalStorage, { type LocalStorageData } from '#/utilities/LocalStorage'
4949
import { unsafeEntries } from 'enso-common/src/utilities/data/object'
5050

5151
/**
@@ -60,6 +60,10 @@ export function EnsoDevtools() {
6060
const enableVersionChecker = useEnableVersionChecker()
6161
const setEnableVersionChecker = useSetEnableVersionChecker()
6262
const { localStorage } = useLocalStorage()
63+
const [localStorageState, setLocalStorageState] = React.useState<Partial<LocalStorageData>>({})
64+
65+
// Re-render when localStorage changes.
66+
React.useEffect(() => localStorage.subscribeAll(setLocalStorageState), [localStorage])
6367

6468
const featureFlags = useFeatureFlags()
6569
const setFeatureFlags = useSetFeatureFlags()
@@ -157,46 +161,6 @@ export function EnsoDevtools() {
157161
)}
158162
</ariaComponents.Form>
159163

160-
<Separator orientation="horizontal" className="my-3" />
161-
162-
<div className="mb-2 flex w-full items-center justify-between">
163-
<Text variant="subtitle">{getText('localStorage')}</Text>
164-
165-
<Button
166-
aria-label={getText('deleteAll')}
167-
size="small"
168-
variant="icon"
169-
icon={trash}
170-
onPress={() => {
171-
for (const [key] of unsafeEntries(LocalStorage.keyMetadata)) {
172-
localStorage.delete(key)
173-
}
174-
}}
175-
/>
176-
</div>
177-
178-
<div className="flex flex-col gap-0.5">
179-
{unsafeEntries(LocalStorage.keyMetadata).map(([key]) => (
180-
<div key={key} className="flex w-full items-center justify-between gap-1">
181-
<Text variant="body">
182-
{key
183-
.replace(/[A-Z]/g, (m) => ' ' + m.toLowerCase())
184-
.replace(/^./, (m) => m.toUpperCase())}
185-
</Text>
186-
187-
<Button
188-
variant="icon"
189-
size="small"
190-
aria-label={getText('delete')}
191-
icon={cross}
192-
onPress={() => {
193-
localStorage.delete(key)
194-
}}
195-
/>
196-
</div>
197-
))}
198-
</div>
199-
200164
<ariaComponents.Separator orientation="horizontal" className="my-3" />
201165

202166
<ariaComponents.Text variant="subtitle" className="mb-2">
@@ -292,6 +256,47 @@ export function EnsoDevtools() {
292256
)
293257
})}
294258
</ariaComponents.Form>
259+
260+
<Separator orientation="horizontal" className="my-3" />
261+
262+
<div className="mb-2 flex w-full items-center justify-between">
263+
<Text variant="subtitle">{getText('localStorage')}</Text>
264+
265+
<Button
266+
aria-label={getText('deleteAll')}
267+
size="small"
268+
variant="icon"
269+
icon={TrashIcon}
270+
onPress={() => {
271+
for (const [key] of unsafeEntries(LocalStorage.keyMetadata)) {
272+
localStorage.delete(key)
273+
}
274+
}}
275+
/>
276+
</div>
277+
278+
<div className="flex flex-col gap-0.5">
279+
{unsafeEntries(LocalStorage.keyMetadata).map(([key]) => (
280+
<div key={key} className="flex w-full items-center justify-between gap-1">
281+
<Text variant="body">
282+
{key
283+
.replace(/[A-Z]/g, (m) => ' ' + m.toLowerCase())
284+
.replace(/^./, (m) => m.toUpperCase())}
285+
</Text>
286+
287+
<Button
288+
variant="icon"
289+
size="small"
290+
isDisabled={localStorageState[key] == null}
291+
aria-label={getText('delete')}
292+
icon={CrossIcon}
293+
onPress={() => {
294+
localStorage.delete(key)
295+
}}
296+
/>
297+
</div>
298+
))}
299+
</div>
295300
</Popover>
296301
</ariaComponents.DialogTrigger>
297302
</Portal>

app/dashboard/src/components/styled/SidebarTabButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button, type ButtonProps } from '#/components/AriaComponents'
33
import { tv } from '#/utilities/tailwindVariants'
44

55
const SIDEBAR_TAB_BUTTON_STYLES = tv({
6-
base: 'font-medium',
6+
base: 'z-1 font-medium',
77
variants: {
88
isActive: { true: 'bg-white opacity-100' },
99
},

app/dashboard/src/layouts/Settings/MembersSettingsSection.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
/** @file Settings tab for viewing and editing organization members. */
2-
import * as React from 'react'
3-
42
import { useMutation, useSuspenseQueries } from '@tanstack/react-query'
53

64
import { backendMutationOptions } from '#/hooks/backendHooks'
@@ -92,20 +90,24 @@ export default function MembersSettingsSection() {
9290
<table className="table-fixed self-start rounded-rows">
9391
<thead>
9492
<tr className="h-row">
95-
<th className="w-members-name-column border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
93+
<th className="w-48 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
9694
{getText('name')}
9795
</th>
98-
<th className="w-members-email-column border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
96+
<th className="w-48 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
9997
{getText('status')}
10098
</th>
10199
</tr>
102100
</thead>
103101
<tbody className="select-text">
104102
{members.map((member) => (
105103
<tr key={member.email} className="group h-row rounded-rows-child">
106-
<td className="border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
107-
<span className="block text-sm">{member.email}</span>
108-
<span className="block text-xs text-primary/50">{member.name}</span>
104+
<td className="max-w-48 border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
105+
<ariaComponents.Text truncate="1" className="block">
106+
{member.email}
107+
</ariaComponents.Text>
108+
<ariaComponents.Text truncate="1" className="block text-2xs text-primary/40">
109+
{member.name}
110+
</ariaComponents.Text>
109111
</td>
110112
<td className="border-x-2 border-transparent bg-clip-padding px-cell-x first:rounded-l-full last:rounded-r-full last:border-r-0">
111113
<div className="flex flex-col">

0 commit comments

Comments
 (0)