Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src/api/gql/dataPlanes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { DataPlanesQuery } from 'src/gql-types/graphql';
import type {
DataPlanesQuery,
PublicDataPlanesQuery,
} from 'src/gql-types/graphql';
import type { CloudProvider } from 'src/utils/cloudRegions';

import { graphql } from 'src/gql-types';
Expand Down Expand Up @@ -28,6 +31,27 @@ export const DATA_PLANES_QUERY = graphql(`
}
`);

// Unauthenticated query for the pre-tenant onboarding flow. The
// authenticated `dataPlanes` query returns nothing until the user
// has grants, which a brand-new signup does not.
export const PUBLIC_DATA_PLANES_QUERY = graphql(`
query PublicDataPlanes($after: String) {
publicDataPlanes(first: 100, after: $after) {
edges {
node {
name
cloudProvider
region
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`);

type DataPlaneGqlNode = DataPlanesQuery['dataPlanes']['edges'][number]['node'];

export interface DataPlaneNode extends Omit<DataPlaneGqlNode, 'cloudProvider'> {
Expand All @@ -44,3 +68,21 @@ export const toDataPlaneNode = (node: DataPlaneGqlNode): DataPlaneNode => {
scope: node.isPublic ? 'public' : 'private',
};
};

type PublicDataPlaneGqlNode =
PublicDataPlanesQuery['publicDataPlanes']['edges'][number]['node'];

export interface PublicDataPlaneNode
extends Omit<PublicDataPlaneGqlNode, 'cloudProvider'> {
// Narrower than the schema's DataPlaneCloudProvider, which also allows LOCAL
cloudProvider: CloudProvider;
}

export const toPublicDataPlaneNode = (
node: PublicDataPlaneGqlNode
): PublicDataPlaneNode => {
return {
...node,
cloudProvider: node.cloudProvider as CloudProvider,
};
};
1 change: 1 addition & 0 deletions src/context/URQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function UrqlConfigProvider({ children }: BaseComponentProps) {
RefreshTokenInfo: (_data) => null,
StorageMapping: (data) => null,
DataPlane: (data) => null,
PublicDataPlane: (data) => null,
},
updates: {
Mutation: {
Expand Down
14 changes: 11 additions & 3 deletions src/directives/BetaOnboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { submitDirective } from 'src/api/directives';
import RegistrationProgress from 'src/app/guards/RegistrationProgress';
import BetaWarningAndError from 'src/components/transformation/create/BetaWarningAndError';
import Actions from 'src/directives/Actions';
import DataPlaneSelector from 'src/directives/Onboard/DataPlaneSelector';
import OrganizationNameField from 'src/directives/Onboard/OrganizationName';
import {
useOnboardingStore_nameInvalid,
useOnboardingStore_requestedDataPlane,
useOnboardingStore_requestedTenant,
useOnboardingStore_resetState,
useOnboardingStore_setNameMissing,
Expand All @@ -39,13 +41,15 @@ const EVENT_NAME = 'Tenant:Create';
const submit_onboard = async (
requestedTenant: string,
directive: any,
surveyResponse: any
surveyResponse: any,
requestedDataPlane: string | null
) => {
return submitDirective(
directiveName,
directive,
requestedTenant,
surveyResponse
surveyResponse,
requestedDataPlane
);
};

Expand All @@ -60,6 +64,7 @@ const BetaOnboard = ({ directive, mutate, status }: DirectiveProps) => {
const setNameMissing = useOnboardingStore_setNameMissing();
const setSurveyMissing = useOnboardingStore_setSurveyMissing();
const surveyResponse = useOnboardingStore_surveyResponse();
const requestedDataPlane = useOnboardingStore_requestedDataPlane();
const resetOnboardingState = useOnboardingStore_resetState();
const setServerError = useOnboardingStore_setServerError();

Expand Down Expand Up @@ -100,7 +105,8 @@ const BetaOnboard = ({ directive, mutate, status }: DirectiveProps) => {
const onboardingResponse = await submit_onboard(
requestedTenant,
directive,
surveyResponse
surveyResponse,
requestedDataPlane
);

if (onboardingResponse.error) {
Expand Down Expand Up @@ -194,6 +200,8 @@ const BetaOnboard = ({ directive, mutate, status }: DirectiveProps) => {
>
<OrganizationNameField forceError={nameTaken} />

<DataPlaneSelector />

<OnboardingSurvey />

<Actions
Expand Down
121 changes: 121 additions & 0 deletions src/directives/Onboard/DataPlaneSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { PublicDataPlaneNode } from 'src/api/gql/dataPlanes';

import { useEffect, useMemo } from 'react';

import {
Autocomplete,
FormControl,
FormLabel,
inputBaseClasses,
TextField,
} from '@mui/material';

import { useIntl } from 'react-intl';

import DataPlaneIcon from 'src/components/shared/Entity/DataPlaneIcon';
import {
useOnboardingStore_requestedDataPlane,
useOnboardingStore_setRequestedDataPlane,
} from 'src/directives/Onboard/Store/hooks';
import { usePublicDataPlanes } from 'src/hooks/dataPlanes/usePublicDataPlanes';

// Matches est-dry-dock's phased rollout of colocated trial buckets: this is
// the plane new tenants land on when nothing else is picked.
const DEFAULT_PUBLIC_DATA_PLANE = 'ops/dp/public/aws-us-east-1-c1';

const INPUT_SX = {
maxWidth: 424,
[`& .${inputBaseClasses.root}`]: { borderRadius: 3 },
};

// The region and full catalog name are both shown: multiple public planes can
// share a region, so the name is what makes the choice unambiguous.
const optionLabel = (option: PublicDataPlaneNode) =>
`${option.region} (${option.name})`;

function DataPlaneSelector() {
const intl = useIntl();
const { dataPlanes, loading, error } = usePublicDataPlanes();

const selected = useOnboardingStore_requestedDataPlane();
const setSelected = useOnboardingStore_setRequestedDataPlane();

// Sorted by provider first because groupBy only groups correctly when the
// list is already ordered by group; sorting on name alone worked only
// because the names happen to embed the provider.
const options = useMemo(
() =>
[...dataPlanes].sort(
(a, b) =>
a.cloudProvider.localeCompare(b.cloudProvider) ||
a.region.localeCompare(b.region)
),
[dataPlanes]
);

// Preselect the platform default so submitting without touching the
// picker still records an explicit, valid choice.
useEffect(() => {
if (!selected && options.length > 0) {
const preferred =
options.find(
(option) => option.name === DEFAULT_PUBLIC_DATA_PLANE
) ?? options[0];
setSelected(preferred.name);
}
}, [options, selected, setSelected]);

// Fail safe: if the plane list can't be fetched, render nothing. The
// claim simply omits requestedDataPlane and the backend applies its
// own default instead of blocking signup on this field.
if (error || (!loading && options.length === 0)) {
return null;
}

const currentOption = options.find((option) => option.name === selected);

return (
<FormControl>
<FormLabel id="requestedDataPlane" sx={{ mb: 1, fontSize: 20 }}>
{intl.formatMessage({ id: 'tenant.dataPlane.label' })}
</FormLabel>

<Autocomplete
loading={loading}
options={options}
value={currentOption ?? null}
onChange={(_event, value) => setSelected(value?.name ?? null)}
groupBy={(option) => option.cloudProvider}
getOptionLabel={optionLabel}
renderOption={(props, option) => {
const { key, ...rest } = props;
return (
<li key={key} {...rest}>
<DataPlaneIcon
provider={option.cloudProvider}
scope="public"
size={20}
/>
<span style={{ marginLeft: 8 }}>
{optionLabel(option)}
</span>
</li>
);
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
variant="outlined"
helperText={intl.formatMessage({
id: 'tenant.dataPlane.helper',
})}
sx={INPUT_SX}
/>
)}
/>
</FormControl>
);
}

export default DataPlaneSelector;
12 changes: 12 additions & 0 deletions src/directives/Onboard/Store/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getInitialStateData = (): Pick<
| 'nameProblematic'
| 'nameMissing'
| 'requestedTenant'
| 'requestedDataPlane'
| 'surveyResponse'
| 'surveyMissing'
| 'serverError'
Expand All @@ -25,6 +26,7 @@ const getInitialStateData = (): Pick<
nameProblematic: false,
nameMissing: false,
requestedTenant: '',
requestedDataPlane: null,
surveyResponse: { origin: '', details: '' },
surveyMissing: false,
serverError: null,
Expand Down Expand Up @@ -63,6 +65,16 @@ const getInitialState = (set: NamedSet<OnboardingState>): OnboardingState => ({
);
},

setRequestedDataPlane: (value) => {
set(
() => ({
requestedDataPlane: value,
}),
false,
'setRequestedDataPlane'
);
},

setServerError: (value) => {
set(
(state) => ({
Expand Down
14 changes: 14 additions & 0 deletions src/directives/Onboard/Store/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ export const useOnboardingStore_setRequestedTenant = () => {
>(OnboardingStoreNames.GENERAL, (state) => state.setRequestedTenant);
};

export const useOnboardingStore_requestedDataPlane = () => {
return useLocalZustandStore<
OnboardingState,
OnboardingState['requestedDataPlane']
>(OnboardingStoreNames.GENERAL, (state) => state.requestedDataPlane);
};

export const useOnboardingStore_setRequestedDataPlane = () => {
return useLocalZustandStore<
OnboardingState,
OnboardingState['setRequestedDataPlane']
>(OnboardingStoreNames.GENERAL, (state) => state.setRequestedDataPlane);
};

export const useOnboardingStore_nameInvalid = () => {
return useLocalZustandStore<
OnboardingState,
Expand Down
3 changes: 3 additions & 0 deletions src/directives/Onboard/Store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface OnboardingState {
requestedTenant: string;
setRequestedTenant: (value: string) => void;

requestedDataPlane: string | null;
setRequestedDataPlane: (value: string | null) => void;

nameInvalid: boolean;
setNameInvalid: (value: boolean) => void;

Expand Down
12 changes: 10 additions & 2 deletions src/directives/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,17 @@ export const DIRECTIVES: Directives = {
return queryBuilder;
},
generateUserClaim: (args: any[]) => {
const [requestedTenant, survey, requestedDataPlane] = args;
return {
requestedTenant: args[0],
survey: args.length > 1 ? args[1] : null,
requestedTenant,
survey: survey ?? null,
// Omitted (never sent as null) when unset: the agent's
// claims parser rejects unknown keys, and older agents
// don't know this one — leaving it out lets the backend
// fall back to its own default plane.
...(hasLength(requestedDataPlane)
? { requestedDataPlane }
: {}),
};
},
calculateStatus: (appliedDirective) => {
Expand Down
1 change: 1 addition & 0 deletions src/directives/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface ClickToAcceptClaim {
interface OnboardClaim {
requestedTenant: string;
survey: any;
requestedDataPlane?: string;
}

interface StorageMappingsClaim {
Expand Down
Loading
Loading