mirror of https://github.com/coder/coder.git
feat: add all safe experiments to the deployment page (#10276)
* added new option table type for experiments * added tests * fixed go tests * added go test for new param * removing query change * clearing ExperimentsAll * dont mutate ExperimentsAll * added new route for safe experiments * added new route for safe experiments * added test for new route * PR feedback * altered design * alias children
This commit is contained in:
parent
35f9e2ef7f
commit
1656249e07
|
@ -587,8 +587,36 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"General"
|
||||
],
|
||||
"summary": "Get experiments",
|
||||
"operationId": "get-experiments",
|
||||
"summary": "Get enabled experiments",
|
||||
"operationId": "get-enabled-experiments",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Experiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/experiments/available": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"General"
|
||||
],
|
||||
"summary": "Get safe experiments",
|
||||
"operationId": "get-safe-experiments",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
|
@ -497,8 +497,32 @@
|
|||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["General"],
|
||||
"summary": "Get experiments",
|
||||
"operationId": "get-experiments",
|
||||
"summary": "Get enabled experiments",
|
||||
"operationId": "get-enabled-experiments",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Experiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/experiments/available": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["General"],
|
||||
"summary": "Get safe experiments",
|
||||
"operationId": "get-safe-experiments",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
|
@ -597,6 +597,7 @@ func New(options *Options) *API {
|
|||
})
|
||||
r.Route("/experiments", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/available", handleExperimentsSafe)
|
||||
r.Get("/", api.handleExperimentsGet)
|
||||
})
|
||||
r.Get("/updatecheck", api.updateCheck)
|
||||
|
|
|
@ -4,10 +4,11 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// @Summary Get experiments
|
||||
// @ID get-experiments
|
||||
// @Summary Get enabled experiments
|
||||
// @ID get-enabled-experiments
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags General
|
||||
|
@ -17,3 +18,17 @@ func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) {
|
|||
ctx := r.Context()
|
||||
httpapi.Write(ctx, rw, http.StatusOK, api.Experiments)
|
||||
}
|
||||
|
||||
// @Summary Get safe experiments
|
||||
// @ID get-safe-experiments
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags General
|
||||
// @Success 200 {array} codersdk.Experiment
|
||||
// @Router /experiments/available [get]
|
||||
func handleExperimentsSafe(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AvailableExperiments{
|
||||
Safe: codersdk.ExperimentsAll,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -116,4 +116,21 @@ func Test_Experiments(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, httpmw.SignedOutErrorMessage)
|
||||
})
|
||||
|
||||
t.Run("available experiments", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := coderdtest.DeploymentValues(t)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: cfg,
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
experiments, err := client.SafeExperiments(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, experiments)
|
||||
require.ElementsMatch(t, codersdk.ExperimentsAll, experiments.Safe)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2013,12 +2013,13 @@ var ExperimentsAll = Experiments{
|
|||
ExperimentSingleTailnet,
|
||||
}
|
||||
|
||||
// Experiments is a list of experiments that are enabled for the deployment.
|
||||
// Experiments is a list of experiments.
|
||||
// Multiple experiments may be enabled at the same time.
|
||||
// Experiments are not safe for production use, and are not guaranteed to
|
||||
// be backwards compatible. They may be removed or renamed at any time.
|
||||
type Experiments []Experiment
|
||||
|
||||
// Returns a list of experiments that are enabled for the deployment.
|
||||
func (e Experiments) Enabled(ex Experiment) bool {
|
||||
for _, v := range e {
|
||||
if v == ex {
|
||||
|
@ -2041,6 +2042,25 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) {
|
|||
return exp, json.NewDecoder(res.Body).Decode(&exp)
|
||||
}
|
||||
|
||||
// AvailableExperiments is an expandable type that returns all safe experiments
|
||||
// available to be used with a deployment.
|
||||
type AvailableExperiments struct {
|
||||
Safe []Experiment `json:"safe"`
|
||||
}
|
||||
|
||||
func (c *Client) SafeExperiments(ctx context.Context) (AvailableExperiments, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments/available", nil)
|
||||
if err != nil {
|
||||
return AvailableExperiments{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return AvailableExperiments{}, ReadBodyAsError(res)
|
||||
}
|
||||
var exp AvailableExperiments
|
||||
return exp, json.NewDecoder(res.Body).Decode(&exp)
|
||||
}
|
||||
|
||||
type DAUsResponse struct {
|
||||
Entries []DAUEntry `json:"entries"`
|
||||
TZHourOffset int `json:"tz_hour_offset"`
|
||||
|
|
|
@ -535,7 +535,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \
|
|||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get experiments
|
||||
## Get enabled experiments
|
||||
|
||||
### Code samples
|
||||
|
||||
|
@ -562,7 +562,44 @@ curl -X GET http://coder-server:8080/api/v2/experiments \
|
|||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Experiment](schemas.md#codersdkexperiment) |
|
||||
|
||||
<h3 id="get-experiments-responseschema">Response Schema</h3>
|
||||
<h3 id="get-enabled-experiments-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ----- | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get safe experiments
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/experiments/available \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /experiments/available`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
["moons"]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Experiment](schemas.md#codersdkexperiment) |
|
||||
|
||||
<h3 id="get-safe-experiments-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
|
|
|
@ -864,6 +864,19 @@ export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getAvailableExperiments =
|
||||
async (): Promise<TypesGen.AvailableExperiments> => {
|
||||
try {
|
||||
const response = await axios.get("/api/v2/experiments/available");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
return { safe: [] };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getExternalAuthProvider = async (
|
||||
provider: string,
|
||||
): Promise<TypesGen.ExternalAuth> => {
|
||||
|
|
|
@ -19,3 +19,10 @@ export const experiments = (queryClient: QueryClient) => {
|
|||
},
|
||||
} satisfies UseQueryOptions<Experiments>;
|
||||
};
|
||||
|
||||
export const availableExperiments = () => {
|
||||
return {
|
||||
queryKey: ["availableExperiments"],
|
||||
queryFn: async () => API.getAvailableExperiments(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -151,6 +151,11 @@ export interface AuthorizationRequest {
|
|||
// From codersdk/authorization.go
|
||||
export type AuthorizationResponse = Record<string, boolean>;
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface AvailableExperiments {
|
||||
readonly safe: Experiment[];
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface BuildInfoResponse {
|
||||
readonly external_url: string;
|
||||
|
|
|
@ -4,6 +4,7 @@ import Box, { BoxProps } from "@mui/material/Box";
|
|||
import { useTheme } from "@mui/system";
|
||||
import { DisabledBadge, EnabledBadge } from "./Badges";
|
||||
import { css } from "@emotion/react";
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
|
||||
|
||||
export const OptionName: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props;
|
||||
|
@ -38,11 +39,11 @@ export const OptionDescription: FC<PropsWithChildren> = (props) => {
|
|||
};
|
||||
|
||||
interface OptionValueProps {
|
||||
children?: boolean | number | string | string[];
|
||||
children?: boolean | number | string | string[] | Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
const { children } = props;
|
||||
const { children: value } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const optionStyles = css`
|
||||
|
@ -56,35 +57,74 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
}
|
||||
`;
|
||||
|
||||
if (typeof children === "boolean") {
|
||||
return children ? <EnabledBadge /> : <DisabledBadge />;
|
||||
const listStyles = css`
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
flex-direction: "column",
|
||||
gap: theme.spacing(0.5),
|
||||
`;
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? <EnabledBadge /> : <DisabledBadge />;
|
||||
}
|
||||
|
||||
if (typeof children === "number") {
|
||||
return <span css={optionStyles}>{children}</span>;
|
||||
if (typeof value === "number") {
|
||||
return <span css={optionStyles}>{value}</span>;
|
||||
}
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
if (!value || value.length === 0) {
|
||||
return <span css={optionStyles}>Not set</span>;
|
||||
}
|
||||
|
||||
if (typeof children === "string") {
|
||||
return <span css={optionStyles}>{children}</span>;
|
||||
if (typeof value === "string") {
|
||||
return <span css={optionStyles}>{value}</span>;
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
return (
|
||||
<ul
|
||||
css={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStylePosition: "inside",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
{children.map((item) => (
|
||||
<ul css={listStyles && { listStyle: "none" }}>
|
||||
{Object.entries(value)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([option, isEnabled]) => (
|
||||
<li
|
||||
key={option}
|
||||
css={[
|
||||
optionStyles,
|
||||
!isEnabled && {
|
||||
marginLeft: 32,
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{isEnabled && (
|
||||
<CheckCircleOutlined
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) => theme.palette.success.light,
|
||||
margin: (theme) => theme.spacing(0, 1),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{option}
|
||||
</Box>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<ul css={listStyles && { listStylePosition: "inside" }}>
|
||||
{value.map((item) => (
|
||||
<li key={item} css={optionStyles}>
|
||||
{item}
|
||||
</li>
|
||||
|
@ -93,7 +133,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
return <span css={optionStyles}>{JSON.stringify(children)}</span>;
|
||||
return <span css={optionStyles}>{JSON.stringify(value)}</span>;
|
||||
};
|
||||
|
||||
interface OptionConfigProps extends BoxProps {
|
||||
|
|
|
@ -19,7 +19,8 @@ import { optionValue } from "./optionValue";
|
|||
|
||||
const OptionsTable: FC<{
|
||||
options: ClibaseOption[];
|
||||
}> = ({ options }) => {
|
||||
additionalValues?: string[];
|
||||
}> = ({ options, additionalValues }) => {
|
||||
if (options.length === 0) {
|
||||
return <p>No options to configure</p>;
|
||||
}
|
||||
|
@ -95,7 +96,9 @@ const OptionsTable: FC<{
|
|||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<OptionValue>{optionValue(option)}</OptionValue>
|
||||
<OptionValue>
|
||||
{optionValue(option, additionalValues)}
|
||||
</OptionValue>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ const defaultOption: ClibaseOption = {
|
|||
describe("optionValue", () => {
|
||||
it.each<{
|
||||
option: ClibaseOption;
|
||||
additionalValues?: string[];
|
||||
expected: unknown;
|
||||
}>([
|
||||
{
|
||||
|
@ -67,7 +68,46 @@ describe("optionValue", () => {
|
|||
},
|
||||
expected: [`"123"->"foo"`, `"456"->"bar"`, `"789"->"baz"`],
|
||||
},
|
||||
])(`[$option.name]optionValue($option.value)`, ({ option, expected }) => {
|
||||
expect(optionValue(option)).toEqual(expected);
|
||||
});
|
||||
{
|
||||
option: {
|
||||
...defaultOption,
|
||||
name: "Experiments",
|
||||
value: ["single_tailnet"],
|
||||
},
|
||||
additionalValues: ["single_tailnet", "deployment_health_page"],
|
||||
expected: { single_tailnet: true, deployment_health_page: false },
|
||||
},
|
||||
{
|
||||
option: {
|
||||
...defaultOption,
|
||||
name: "Experiments",
|
||||
value: [],
|
||||
},
|
||||
additionalValues: ["single_tailnet", "deployment_health_page"],
|
||||
expected: { single_tailnet: false, deployment_health_page: false },
|
||||
},
|
||||
{
|
||||
option: {
|
||||
...defaultOption,
|
||||
name: "Experiments",
|
||||
value: ["moons"],
|
||||
},
|
||||
additionalValues: ["single_tailnet", "deployment_health_page"],
|
||||
expected: { single_tailnet: false, deployment_health_page: false },
|
||||
},
|
||||
{
|
||||
option: {
|
||||
...defaultOption,
|
||||
name: "Experiments",
|
||||
value: ["*"],
|
||||
},
|
||||
additionalValues: ["single_tailnet", "deployment_health_page"],
|
||||
expected: { single_tailnet: true, deployment_health_page: true },
|
||||
},
|
||||
])(
|
||||
`[$option.name]optionValue($option.value)`,
|
||||
({ option, expected, additionalValues }) => {
|
||||
expect(optionValue(option, additionalValues)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,10 @@ import { ClibaseOption } from "api/typesGenerated";
|
|||
import { intervalToDuration, formatDuration } from "date-fns";
|
||||
|
||||
// optionValue is a helper function to format the value of a specific deployment options
|
||||
export function optionValue(option: ClibaseOption) {
|
||||
export function optionValue(
|
||||
option: ClibaseOption,
|
||||
additionalValues?: string[],
|
||||
) {
|
||||
switch (option.name) {
|
||||
case "Max Token Lifetime":
|
||||
case "Session Duration":
|
||||
|
@ -19,6 +22,27 @@ export function optionValue(option: ClibaseOption) {
|
|||
return Object.entries(option.value as Record<string, string>).map(
|
||||
([key, value]) => `"${key}"->"${value}"`,
|
||||
);
|
||||
case "Experiments": {
|
||||
const experimentMap: Record<string, boolean> | undefined =
|
||||
additionalValues?.reduce(
|
||||
(acc, v) => {
|
||||
return { ...acc, [v]: option.value.includes("*") ? true : false };
|
||||
},
|
||||
{} as Record<string, boolean>,
|
||||
);
|
||||
|
||||
if (!experimentMap) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const v of option.value) {
|
||||
if (Object.hasOwn(experimentMap, v)) {
|
||||
experimentMap[v] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return experimentMap;
|
||||
}
|
||||
default:
|
||||
return option.value;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useQuery } from "react-query";
|
|||
import { pageTitle } from "utils/page";
|
||||
import { deploymentDAUs } from "api/queries/deployment";
|
||||
import { entitlements } from "api/queries/entitlements";
|
||||
import { availableExperiments } from "api/queries/experiments";
|
||||
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout";
|
||||
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
|
||||
|
||||
|
@ -11,6 +12,7 @@ const GeneralSettingsPage: FC = () => {
|
|||
const { deploymentValues } = useDeploySettings();
|
||||
const deploymentDAUsQuery = useQuery(deploymentDAUs());
|
||||
const entitlementsQuery = useQuery(entitlements());
|
||||
const experimentsQuery = useQuery(availableExperiments());
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -22,6 +24,7 @@ const GeneralSettingsPage: FC = () => {
|
|||
deploymentDAUs={deploymentDAUsQuery.data}
|
||||
deploymentDAUsError={deploymentDAUsQuery.error}
|
||||
entitlements={entitlementsQuery.data}
|
||||
safeExperiments={experimentsQuery.data?.safe ?? []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -34,12 +34,13 @@ const meta: Meta<typeof GeneralSettingsPageView> = {
|
|||
description:
|
||||
"Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.",
|
||||
flag: "experiments",
|
||||
value: ["*", "moons", "single_tailnet", "deployment_health_page"],
|
||||
value: ["single_tailnet"],
|
||||
flag_shorthand: "",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
deploymentDAUs: MockDeploymentDAUResponse,
|
||||
safeExperiments: ["single_tailnet", "deployment_health_page"],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -69,3 +70,38 @@ export const DAUError: Story = {
|
|||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const allExperimentsEnabled: Story = {
|
||||
args: {
|
||||
deploymentOptions: [
|
||||
{
|
||||
name: "Access URL",
|
||||
description:
|
||||
"The URL that users will use to access the Coder deployment.",
|
||||
flag: "access-url",
|
||||
flag_shorthand: "",
|
||||
value: "https://dev.coder.com",
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: "Wildcard Access URL",
|
||||
description:
|
||||
'Specifies the wildcard hostname to use for workspace applications in the form "*.example.com".',
|
||||
flag: "wildcard-access-url",
|
||||
flag_shorthand: "",
|
||||
value: "*--apps.dev.coder.com",
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: "Experiments",
|
||||
description:
|
||||
"Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.",
|
||||
flag: "experiments",
|
||||
value: ["*"],
|
||||
flag_shorthand: "",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
safeExperiments: ["single_tailnet", "deployment_health_page"],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import { ClibaseOption, DAUsResponse, Entitlements } from "api/typesGenerated";
|
||||
import {
|
||||
ClibaseOption,
|
||||
DAUsResponse,
|
||||
Entitlements,
|
||||
Experiments,
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import {
|
||||
ActiveUserChart,
|
||||
|
@ -17,12 +22,14 @@ export type GeneralSettingsPageViewProps = {
|
|||
deploymentDAUs?: DAUsResponse;
|
||||
deploymentDAUsError: unknown;
|
||||
entitlements: Entitlements | undefined;
|
||||
safeExperiments: Experiments | undefined;
|
||||
};
|
||||
export const GeneralSettingsPageView = ({
|
||||
deploymentOptions,
|
||||
deploymentDAUs,
|
||||
deploymentDAUsError,
|
||||
entitlements,
|
||||
safeExperiments,
|
||||
}: GeneralSettingsPageViewProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
|
@ -57,6 +64,7 @@ export const GeneralSettingsPageView = ({
|
|||
"Wildcard Access URL",
|
||||
"Experiments",
|
||||
)}
|
||||
additionalValues={safeExperiments}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
|
|
Loading…
Reference in New Issue