feat: add --experiments flag to replace --experimental (#5767)

- Deprecates the --experimental flag
- Adds a new flag --experiments which supports passing multiple comma-separated values or a wildcard value.
- Exposes a new endpoint /api/v2/experiments that returns the list of enabled experiments.
- Deprecates the field Features.Experimental in favour of this new API.
- Updates apidocgen to support type aliases (shoutout to @mtojek).
- Modifies apitypings to support generating slice types.
- Updates develop.sh to pass additional args after -- to $CODERD_SHIM.
This commit is contained in:
Cian Johnston
2023-01-18 19:12:53 +00:00
committed by GitHub
parent 47c3d72294
commit 56b996532f
29 changed files with 593 additions and 41 deletions
+12
View File
@@ -636,6 +636,18 @@ export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
}
}
export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
try {
const response = await axios.get("/api/v2/experiments")
return response.data
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return []
}
throw error
}
}
export const getAuditLogs = async (
options: TypesGen.AuditLogsRequest,
): Promise<TypesGen.AuditLogResponse> => {
+10 -2
View File
@@ -311,11 +311,12 @@ export interface DeploymentConfig {
readonly scim_api_key: DeploymentConfigField<string>
readonly provisioner: ProvisionerConfig
readonly rate_limit: RateLimitConfig
readonly experimental: DeploymentConfigField<boolean>
readonly experiments: DeploymentConfigField<string[]>
readonly update_check: DeploymentConfigField<boolean>
readonly max_token_lifetime: DeploymentConfigField<number>
readonly swagger: SwaggerConfig
readonly logging: LoggingConfig
readonly experimental: DeploymentConfigField<boolean>
}
// From codersdk/deploymentconfig.go
@@ -337,10 +338,13 @@ export interface Entitlements {
readonly warnings: string[]
readonly errors: string[]
readonly has_license: boolean
readonly experimental: boolean
readonly trial: boolean
readonly experimental: boolean
}
// From codersdk/experiments.go
export type Experiments = Experiment[]
// From codersdk/features.go
export interface Feature {
readonly entitlement: Entitlement
@@ -1079,6 +1083,10 @@ export const Entitlements: Entitlement[] = [
"not_entitled",
]
// From codersdk/experiments.go
export type Experiment = "vscode_local"
export const Experiments: Experiment[] = ["vscode_local"]
// From codersdk/features.go
export type FeatureName =
| "appearance"
@@ -41,9 +41,9 @@ export const WorkspaceReadyPage = ({
workspaceState.children["scheduleBannerMachine"],
)
const xServices = useContext(XServiceContext)
const experimental = useSelector(
xServices.entitlementsXService,
(state) => state.context.entitlements.experimental,
const experiments = useSelector(
xServices.experimentsXService,
(state) => state.context.experiments || [],
)
const featureVisibility = useSelector(
xServices.entitlementsXService,
@@ -124,7 +124,8 @@ export const WorkspaceReadyPage = ({
canUpdateWorkspace={canUpdateWorkspace}
hideSSHButton={featureVisibility["browser_only"]}
hideVSCodeDesktopButton={
!experimental || featureVisibility["browser_only"]
!experiments.includes("vscode_local") ||
featureVisibility["browser_only"]
}
workspaceErrors={{
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
+2
View File
@@ -982,6 +982,8 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = {
}),
}
export const MockExperiments: TypesGen.Experiment[] = ["vscode_local"]
export const MockAuditLog: TypesGen.AuditLog = {
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
request_id: "53bded77-7b9d-4e82-8771-991a34d759f9",
+5
View File
@@ -15,6 +15,11 @@ export const handlers = [
return res(ctx.status(200), ctx.json(M.MockBuildInfo))
}),
// experiments
rest.get("/api/v2/experiments", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockExperiments))
}),
// update check
rest.get("/api/v2/updatecheck", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockUpdateCheck))
+3
View File
@@ -4,12 +4,14 @@ import { ActorRefFrom } from "xstate"
import { authMachine } from "./auth/authXService"
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
import { entitlementsMachine } from "./entitlements/entitlementsXService"
import { experimentsMachine } from "./experiments/experimentsMachine"
import { appearanceMachine } from "./appearance/appearanceXService"
interface XServiceContextType {
authXService: ActorRefFrom<typeof authMachine>
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
entitlementsXService: ActorRefFrom<typeof entitlementsMachine>
experimentsXService: ActorRefFrom<typeof experimentsMachine>
appearanceXService: ActorRefFrom<typeof appearanceMachine>
}
@@ -30,6 +32,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
authXService: useInterpret(authMachine),
buildInfoXService: useInterpret(buildInfoMachine),
entitlementsXService: useInterpret(entitlementsMachine),
experimentsXService: useInterpret(experimentsMachine),
appearanceXService: useInterpret(appearanceMachine),
}}
>
@@ -22,6 +22,7 @@ const emptyEntitlements = {
features: withDefaultFeatures({}),
has_license: false,
experimental: false,
experimental_features: [],
trial: false,
}
@@ -0,0 +1,75 @@
import { getExperiments } from "api/api"
import { Experiment } from "api/typesGenerated"
import { createMachine, assign } from "xstate"
export interface ExperimentsContext {
experiments?: Experiment[]
getExperimentsError?: Error | unknown
}
export const experimentsMachine = createMachine(
{
id: "experimentsState",
predictableActionArguments: true,
tsTypes: {} as import("./experimentsMachine.typegen").Typegen0,
schema: {
context: {} as ExperimentsContext,
services: {} as {
getExperiments: {
data: Experiment[]
}
},
},
context: {
experiments: undefined,
},
initial: "gettingExperiments",
states: {
gettingExperiments: {
invoke: {
src: "getExperiments",
id: "getExperiments",
onDone: [
{
actions: ["assignExperiments", "clearGetExperimentsError"],
target: "#experimentsState.success",
},
],
onError: [
{
actions: ["assignGetExperimentsError", "clearExperiments"],
target: "#experimentsState.failure",
},
],
},
},
success: {
type: "final",
},
failure: {
type: "final",
},
},
},
{
services: {
getExperiments: getExperiments,
},
actions: {
assignExperiments: assign({
experiments: (_, event) => event.data,
}),
clearExperiments: assign((context: ExperimentsContext) => ({
...context,
experiments: undefined,
})),
assignGetExperimentsError: assign({
getExperimentsError: (_, event) => event.data,
}),
clearGetExperimentsError: assign((context: ExperimentsContext) => ({
...context,
getExperimentsError: undefined,
})),
},
},
)