-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1338 from entur/sandbox-features
Sandbox features
- Loading branch information
Showing
12 changed files
with
397 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { OidcClientSettings } from 'oidc-client-ts'; | ||
import { FlexibleLineType } from '../model/FlexibleLine'; | ||
import { Locale } from '../i18n'; | ||
|
||
/** | ||
* All sandbox features should be added to this interface like this: | ||
* - featureName: boolean; | ||
* | ||
* For multi-level features, only the top-level featureName should be | ||
* toggled. | ||
*/ | ||
export interface SandboxFeatures {} | ||
|
||
export interface Config { | ||
/** | ||
* Base URL for backend GraphQL API (a.k.a. uttu) | ||
* | ||
* Note that uttu has multiple GraphQL endpoints, one for each | ||
* provider and additionally one for the providers API. | ||
*/ | ||
uttuApiUrl?: string; | ||
|
||
/** | ||
* Configure OpenID Connect for authenticating users with a compliant authentication provider | ||
* | ||
* {@see https://authts.github.io/oidc-client-ts/interfaces/OidcClientSettings.html} | ||
*/ | ||
oidcConfig?: OidcClientSettings; | ||
|
||
/** | ||
* Prefix used in XML namespace for providers in exported datasets | ||
*/ | ||
xmlnsUrlPrefix?: string; | ||
|
||
/** | ||
* Enables Entur specific legacy bevaior for filtering authorities and operators. | ||
* {@see ../model/Organisation.ts} | ||
*/ | ||
enableLegacyOrganisationsFilter?: boolean; | ||
|
||
/** | ||
* Optionally restrict available flexible line types available for users to choose from when | ||
* creating flexible lines. | ||
*/ | ||
supportedFlexibleLineTypes?: FlexibleLineType[]; | ||
|
||
/** | ||
* The exact shape of the admin role used to match against role claims in token, toggles | ||
* visibility of providers admin menu option. | ||
* | ||
* This is technical debt, and will be moved to the backend | ||
*/ | ||
adminRole?: string; | ||
|
||
/** | ||
* Namespace for preferred name in tokens | ||
* | ||
* This is technical debt, and will be moved to the backend | ||
*/ | ||
preferredNameNamespace?: string; | ||
|
||
/** | ||
* Namespace for claims in tokens | ||
* | ||
* This is technical debt, and will be moved to the backend | ||
*/ | ||
claimsNamespace?: string; | ||
|
||
/** | ||
* Optional DSN for sentry configuration. If not present, Sentry will not be configured | ||
* {@see https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/} | ||
*/ | ||
sentryDSN?: string; | ||
|
||
/** | ||
* Default locale to use for translations and formatting | ||
*/ | ||
defaultLocale?: Locale; | ||
|
||
/** | ||
* Optionally restrict the choice of locales to the user | ||
*/ | ||
supportedLocales?: Locale[]; | ||
|
||
sandboxFeatures?: SandboxFeatures; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# Sandbox features in enki | ||
|
||
Sandbox features allows development of new functionality with less risk and without affecting | ||
other installations. Sandbox features are controlled by feature flags. The SandboxFeature component | ||
is designed to support code splitting, so that each feature will be compiled into separate chunks. React | ||
will then postpone the downloading of any given chunk until it decides it's time to render the component | ||
inside. | ||
|
||
## How to develop a sandbox feature | ||
|
||
Sandbox features are placed in a folder with the same name as the feature. The feature name should be added | ||
to the SandboxFeatures interface. | ||
|
||
The folder should have an index.ts, with a default export. The default | ||
export should be the main entry (React) component of your sandbox feature. | ||
|
||
Example with a feature called `foobar`: | ||
|
||
// ext/foobar/index.ts | ||
const Foobar: SandboxComponent<FoobarProps> = (props) => { | ||
return ( | ||
<h1>{props.foo}</h1> | ||
) | ||
}; | ||
|
||
export default Foobar; | ||
|
||
The folder must also have | ||
a types.d.ts file which exports the props type declaration for your component. | ||
|
||
// ext/foobar/types.d.ts | ||
export type FoobarProps extends SandboxFeatureProps = { | ||
foo: string; | ||
} | ||
|
||
This ensures type safety across the SandboxFeature wrapper without having an explicit dependency | ||
to your component's runtime code. | ||
|
||
To use your sandbox feature in the main code, you'll use the SandboxFeature component | ||
to wrap it: | ||
|
||
<SandboxFeature<FoobarProps> | ||
feature="foobar" | ||
foo="bar" | ||
/> | ||
|
||
If "foobar" is `false` in your feature flags configuration, this will not render anything. | ||
If "foobar" is `true` it will render: | ||
|
||
<h1>bar</1> | ||
|
||
A `renderFallback` function prop is also available to give the option to render something else | ||
if the feature is not enabled: | ||
|
||
<SandboxFeature<FoobarProps> | ||
feature="foobar" | ||
foo="bar" | ||
renderFallback={() => <h1>foo</h1>} | ||
/> | ||
|
||
will render | ||
|
||
<h1>foo</h1> | ||
|
||
if feature `foobar` is not enabled. | ||
|
||
## How features are controlled by configuration | ||
|
||
First of all, you must add each feature to the `SandboxFeatures` interface in `../config/config.ts`: | ||
|
||
interface SandboxFeatures { | ||
foobar: boolean; | ||
} | ||
|
||
The `sandboxFeatures` property of the bootstrap configuration controls each individual feature. By default, | ||
all features are turned off, and must be explicitly set to be enabled: | ||
|
||
{ | ||
"sandboxFeatures": { | ||
"foobar": true | ||
} | ||
} | ||
|
||
## Nested features | ||
|
||
`SandboxFeature` supports nesting features 2 levels deep. Meaning, you can group several features into one | ||
mega-feature, and configure them as one. They will also be chunked together as one file. | ||
|
||
Example, given the following folder structure: | ||
|
||
// ext/foobar/foo/ | ||
// ext/foobar/bar/ | ||
|
||
And the following feature definition: | ||
|
||
foobar: boolean; | ||
|
||
and configuration setting: | ||
|
||
foobar: true | ||
|
||
You can reference each sub-level feature as follows: | ||
|
||
<SandboxFeature<FoobarProps> | ||
feature="foobar/foo" | ||
foo="bar" | ||
/> | ||
|
||
and | ||
|
||
<SandboxFeature<FoobarProps> | ||
feature="foobar/bar" | ||
bar="foo" | ||
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { FC, lazy, memo, ReactNode, Suspense, useMemo } from 'react'; | ||
import { useConfig } from '../config/ConfigContext'; | ||
import { SandboxFeatures } from '../config/config'; | ||
|
||
/** | ||
* The base props interface for the SandboxFeature component. It is a generic interface that | ||
* takes a single type parameter `Features`, and has a single property `feature` which can | ||
* have the value of any key of `Features`. | ||
*/ | ||
export interface SandboxFeatureProps<Features> { | ||
feature: keyof Features; | ||
renderFallback?: () => ReactNode; | ||
} | ||
|
||
/** | ||
* A type that describes a sandbox component which can be wrapped with the SandboxFeature component. | ||
* It is a generic type which takes two type parameters: `Features` and `Props`. The `Props` type | ||
* should implement the SandboxFeatureProps interface. The resulting type is a Functional Component | ||
* with `Props` without "feature". | ||
*/ | ||
export type SandboxComponent< | ||
Features, | ||
Props extends SandboxFeatureProps<Features>, | ||
> = FC<Omit<Props, 'feature' | 'renderFallback'>>; | ||
|
||
/** | ||
* A component that can load a sandbox Component. It is a generic component with the same type | ||
* parameters as the SandboxComponent. It optionally and lazily renders the SandboxComponent | ||
* identified by through the `feature` prop, which identifies the path of the component to load. | ||
*/ | ||
const SandboxFeature = <Features, Props extends SandboxFeatureProps<Features>>({ | ||
feature, | ||
renderFallback, | ||
...props | ||
}: Props) => { | ||
const { sandboxFeatures } = useConfig(); | ||
|
||
const splitFeature = (feature as string).split('/'); | ||
|
||
let Component: SandboxComponent<Features, Props>; | ||
|
||
if (splitFeature.length > 2) { | ||
throw new Error('Max feature depth is 2'); | ||
} else if (splitFeature.length === 2) { | ||
Component = memo( | ||
lazy(() => import(`./${splitFeature[0]}/${splitFeature[1]}/index.ts`)), | ||
); | ||
} else { | ||
Component = memo(lazy(() => import(`./${splitFeature[0]}/index.ts`))); | ||
} | ||
|
||
const featureEnabled = useMemo( | ||
() => | ||
sandboxFeatures && | ||
Object.entries(sandboxFeatures).some(([key, value]) => { | ||
return key.split('/')[0] === splitFeature[0] && value; | ||
}), | ||
[sandboxFeatures, splitFeature], | ||
); | ||
|
||
return ( | ||
<Suspense> | ||
{featureEnabled ? ( | ||
<Component {...props} /> | ||
) : renderFallback ? ( | ||
renderFallback() | ||
) : null} | ||
</Suspense> | ||
); | ||
}; | ||
|
||
export default memo(SandboxFeature) as typeof SandboxFeature< | ||
SandboxFeatures, | ||
any | ||
>; | ||
|
||
export const TestSandboxFeature = memo(SandboxFeature) as typeof SandboxFeature; |
Oops, something went wrong.