Skip to content

Commit 7e90d2e

Browse files
committed
Merge main into breadcrumbs
2 parents 73a4dba + 23beefe commit 7e90d2e

20 files changed

+348
-186
lines changed

app/components/form/SideModalForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ type SideModalFormProps<TFieldValues extends FieldValues> = {
4141
resourceName: string
4242
/** Must be provided with a reason describing why it's disabled */
4343
submitDisabled?: string
44+
45+
// require loading and error so we can't forget to hook them up. there are a
46+
// few forms that don't need them, so we'll use dummy values
47+
4448
/** Error from the API call */
4549
submitError: ApiError | null
46-
loading?: boolean
50+
loading: boolean
51+
4752
/** Only needed if you need to override the default title (Create/Edit ${resourceName}) */
4853
title?: string
4954
subtitle?: ReactNode

app/forms/disk-attach.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function AttachDiskSideModalForm({
3535
onSubmit,
3636
onDismiss,
3737
diskNamesToExclude = [],
38-
loading,
38+
loading = false,
3939
submitError = null,
4040
}: AttachDiskProps) {
4141
const { project } = useProjectSelector()

app/forms/idp/create.tsx

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { useEffect, useState } from 'react'
89
import { useForm } from 'react-hook-form'
910
import { useNavigate } from 'react-router-dom'
1011

@@ -18,12 +19,16 @@ import { SideModalForm } from '~/components/form/SideModalForm'
1819
import { HL } from '~/components/HL'
1920
import { useSiloSelector } from '~/hooks/use-params'
2021
import { addToast } from '~/stores/toast'
22+
import { Checkbox } from '~/ui/lib/Checkbox'
2123
import { FormDivider } from '~/ui/lib/Divider'
24+
import { Message } from '~/ui/lib/Message'
2225
import { SideModal } from '~/ui/lib/SideModal'
2326
import { readBlobAsBase64 } from '~/util/file'
27+
import { links } from '~/util/links'
2428
import { pb } from '~/util/path-builder'
2529

2630
import { MetadataSourceField, type IdpCreateFormValues } from './shared'
31+
import { getDelegatedDomain } from './util'
2732

2833
const defaultValues: IdpCreateFormValues = {
2934
type: 'saml',
@@ -62,6 +67,23 @@ export function CreateIdpSideModalForm() {
6267
})
6368

6469
const form = useForm({ defaultValues })
70+
const name = form.watch('name')
71+
72+
const [generateUrl, setGenerateUrl] = useState(true)
73+
74+
useEffect(() => {
75+
// When creating a SAML identity provider connection, the ACS URL that the user enters
76+
// should always be of the form: http(s)://<silo>.sys.<suffix>/login/<silo>/saml/<name>
77+
// where <silo> is the Silo name, <suffix> is the delegated domain assigned to the rack,
78+
// and <name> is the name of the IdP connection
79+
// The user can override this by unchecking the "Automatically generate ACS URL" checkbox
80+
// and entering a custom ACS URL, though if they check the box again, we will regenerate
81+
// the ACS URL.
82+
const suffix = getDelegatedDomain(window.location)
83+
if (generateUrl) {
84+
form.setValue('acsUrl', `https://${silo}.sys.${suffix}/login/${silo}/saml/${name}`)
85+
}
86+
}, [form, name, silo, generateUrl])
6587

6688
return (
6789
<SideModalForm
@@ -108,7 +130,35 @@ export function CreateIdpSideModalForm() {
108130
submitError={createIdp.error}
109131
submitLabel="Create provider"
110132
>
111-
<NameField name="name" control={form.control} />
133+
<Message
134+
content={
135+
<>
136+
Read the{' '}
137+
<a
138+
href={links.identityProvidersDocs}
139+
className="underline"
140+
target="_blank"
141+
rel="noreferrer"
142+
>
143+
Rack Configuration
144+
</a>{' '}
145+
guide to learn more about setting up an identity provider.
146+
</>
147+
}
148+
/>
149+
<NameField
150+
name="name"
151+
control={form.control}
152+
description={
153+
<>
154+
A short name for the provider in our system. Users will see it in the path to
155+
the login page:{' '}
156+
<code>
157+
/login/{silo}/saml/{name.trim() || 'idp-name'}
158+
</code>
159+
</>
160+
}
161+
/>
112162
<DescriptionField name="description" control={form.control} required />
113163
<TextField
114164
name="technicalContactEmail"
@@ -127,13 +177,30 @@ export function CreateIdpSideModalForm() {
127177
required
128178
control={form.control}
129179
/>
130-
<TextField
131-
name="acsUrl"
132-
label="ACS URL"
133-
description="Service provider endpoint for the IdP to send the SAML response"
134-
required
135-
control={form.control}
136-
/>
180+
<div className="flex flex-col gap-2">
181+
<TextField
182+
name="acsUrl"
183+
label="ACS URL"
184+
description={
185+
<div className="children:inline-block">
186+
<span>
187+
Oxide endpoint for the identity provider to send the SAML response.{' '}
188+
</span>
189+
<span>
190+
URL is generated from the current hostname, silo name, and provider name
191+
according to a standard format.
192+
</span>
193+
</div>
194+
}
195+
required
196+
control={form.control}
197+
disabled={generateUrl}
198+
copyable
199+
/>
200+
<Checkbox checked={generateUrl} onChange={(e) => setGenerateUrl(e.target.checked)}>
201+
Use standard ACS URL
202+
</Checkbox>
203+
</div>
137204
<TextField
138205
name="sloUrl"
139206
label="Single Logout (SLO) URL"
@@ -142,14 +209,17 @@ export function CreateIdpSideModalForm() {
142209
control={form.control}
143210
/>
144211

212+
<FormDivider />
213+
214+
<SideModal.Heading>Request signing</SideModal.Heading>
145215
{/* We don't bother validating that you have both of these or neither even
146216
though the API requires that because we are going to change the API to
147217
always require both, at which point these become simple `required` fields */}
148218
<FileField
149219
id="public-cert-file-input"
150220
name="signingKeypair.publicCert"
151221
description="DER-encoded X.509 certificate"
152-
label="Public cert"
222+
label="Public certificate"
153223
control={form.control}
154224
/>
155225
<FileField

app/forms/idp/edit.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function EditIdpSideModalForm() {
5858
}
5959
// TODO: pass actual error when this form is hooked up
6060
submitError={null}
61+
loading={false}
6162
>
6263
<PropertiesTable>
6364
<PropertiesTable.Row label="ID">
@@ -79,6 +80,7 @@ export function EditIdpSideModalForm() {
7980
required
8081
control={form.control}
8182
disabled
83+
copyable
8284
/>
8385

8486
<FormDivider />

app/forms/idp/util.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
import { describe, expect, it } from 'vitest'
9+
10+
import { getDelegatedDomain } from './util'
11+
12+
describe('getDomainSuffix', () => {
13+
it('handles arbitrary URLs by falling back to placeholder', () => {
14+
expect(getDelegatedDomain({ hostname: 'localhost' })).toBe('placeholder')
15+
expect(getDelegatedDomain({ hostname: 'console-preview.oxide.computer' })).toBe(
16+
'placeholder'
17+
)
18+
})
19+
20+
it('handles 1 subdomain after sys', () => {
21+
const location = { hostname: 'oxide.sys.r3.oxide-preview.com' }
22+
expect(getDelegatedDomain(location)).toBe('r3.oxide-preview.com')
23+
})
24+
25+
it('handles 2 subdomains after sys', () => {
26+
const location = { hostname: 'oxide.sys.rack2.eng.oxide.computer' }
27+
expect(getDelegatedDomain(location)).toBe('rack2.eng.oxide.computer')
28+
})
29+
})

app/forms/idp/util.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
// note: this lives in its own file for fast refresh reasons
10+
11+
/**
12+
* When given a full URL hostname for an Oxide silo, return the domain
13+
* (everything after `<silo>.sys.`). Placeholder logic should only apply
14+
* in local dev or Vercel previews.
15+
*/
16+
export const getDelegatedDomain = (location: { hostname: string }) =>
17+
location.hostname.split('.sys.')[1] || 'placeholder'

app/forms/image-edit.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export function EditImageSideModalForm({
8686
}
8787
// TODO: pass actual error when this form is hooked up
8888
submitError={null}
89+
loading={false}
8990
>
9091
<PropertiesTable>
9192
<PropertiesTable.Row label="Shared with">{type}</PropertiesTable.Row>

app/forms/image-from-snapshot.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export function CreateImageFromSnapshotSideModalForm() {
8484
})
8585
}
8686
submitError={createImage.error}
87+
loading={createImage.isPending}
8788
>
8889
<PropertiesTable>
8990
<PropertiesTable.Row label="Snapshot">{data.name}</PropertiesTable.Row>

app/forms/network-interface-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type CreateNetworkInterfaceFormProps = {
4242
export function CreateNetworkInterfaceForm({
4343
onSubmit,
4444
onDismiss,
45-
loading,
45+
loading = false,
4646
submitError = null,
4747
}: CreateNetworkInterfaceFormProps) {
4848
const projectSelector = useProjectSelector()

app/forms/snapshot-create.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function CreateSnapshotSideModalForm() {
7272
createSnapshot.mutate({ query: projectSelector, body: values })
7373
}}
7474
submitError={createSnapshot.error}
75+
loading={createSnapshot.isPending}
7576
>
7677
<NameField name="name" control={form.control} />
7778
<DescriptionField name="description" control={form.control} />

0 commit comments

Comments
 (0)