- Context
- The problem
- How the problem is tackled right now
- What's the Platform team goal?
- Next steps and the feedback loop
- What to expect from this POC
- Basic use cases
- π As a developer, I want to show a feature only if it's enabled
- π As a developer, I want to refresh some data related to the Hasura Plan
- π As a developer, I want to be sure my component is not rendered until the Hasura Plan data is available because I do not want to manage the loading state
- π As a developer, I want to test my component in all its versions in Storybook
- Advanced use cases
- π As a developer, I want to add one feature to the catalogue of features managed by
useIsFeatureEnabled
(Neon, for instance) - π As a developer, I want to add one more async source of Hasura Plan info
- π As a developer, I want to add one more Console type
- π As a developer, I want to manage the dynamic route for my feature
- π As a developer, I want to add one feature to the catalogue of features managed by
- Three levels of abstraction
- FAQ
- How can I play with the demo?
- 5-min intro
Context
- The Hasura Console can be loaded in six different "types", and two different "modes" (12 cases to manage). This is messy represented by the
window.__env
object the Hasura Console finds when loads. - When the Console is launched by the CLI server, we must care about the old CLI server and the new one (the cases to manage grows to 18).
- There are additional things to consider that are retrieved dynamically
- Lux entitlements
- Pricing tiers
- EE Trial license
- ... and that's just the beginning...
(here is a good summary by @beaussan about the current situation).
All of the above-mentioned points concur in showing some features or not, in trying to upsell Hasura to the customers, etc.
The problem
The mess with window.__env
and the above cases is already high, introducing a lot of bugs and PRs in the Console.
The engineers working on the Console not only need to understand when a feature is enabled or not but also to understand why in order to offer the best possible UX (in upselling terms too) to the customers.
How the problem is tackled right now
- Through the
proConsole
module where we have been hidden checking the console type/mode in the last six months. - Through using the above utilities combined with custom checks, here is an example.
What's the Platform team goal?
Essentially, to ease the developer life dealing with the above mess. This is possible through
- Centralizing manage all the data/info/properties/vars that impact what the Console shows
- Allow this centralized management to scale for future needs
- Hide all the
messimplementation details of dealing with thewindow.__env
, pricing plans, entitlements, etc.
Next steps and the feedback loop
The next steps are all about the fastest possible feedback loop.
- β Implementing the POC
- π§ Gathering feedback from the ones who mostly requested it
- β³ Adjusting the APIs with the gathered feedback
- β³ Implementing a basic version in the Console to manage the EE Lite/EE Trials cases
- β³ Gathering feedback
- β³ Widening the APIs to the Cloud entitlements cases
- β³ Gathering feedback
- β³ Widening the APIs to the rest of the cases (prioritizing them)
This POC includes:
- a proposal of the TS APIs needed to hide/show a feature
- a proposal to identify the reasons why a feature is enabled/disabled
- a proposal to hide the previous APIs with a dedicated feature-only API
- some simplified version of the use cases the Console must deal with
- some mocks and utils to simulate the Console running in different modes and see the fake features reacting to the changes
This POC does not include:
- battle-tested code
- Storybook utilities
- definitive libraries and file system
- some thorough real use cases the Console must deal with
Please remember that the API design and names are open to be discussed!
// File: features/Prometheus/Prometheus.tsx
// React component version
function Prometheus() {
return (
<IsFeatureEnabled feature="prometheus">
<div>Prometheus</div>
</IsFeatureEnabled>
);
}
// React hook version
function Prometheus() {
const {
status
} = useIsFeatureEnabled('prometheus');
if(status === 'disabled') return null
return <div>Prometheus</div>
}
And also, I want to know why an existing feature is enabled or not.
// File: features/Prometheus/Prometheus.tsx
// React component version
function Prometheus() {
return (
- <IsFeatureEnabled feature="prometheus">
+ <IsFeatureEnabled
+ feature="prometheus"
+ ifDisabled={(reasons: { doNotMatch }) => {
+ if (doNotMatch.eeLite) {
+ return <div>Prometheus is enabled for EE Lite only</div>
+ }
+ }}
+ >
<div>Prometheus</div>
</IsFeatureEnabled>
);
}
// React hook version
function Prometheus() {
const {
status,
+ reasons: { doNotMatch }
} = useIsFeatureEnabled('prometheus');
- if(status === 'disabled') return null
+ if(status === 'disabled') {
+ if (doNotMatch.eeLite) {
+ return <div>Prometheus is enabled for EE Lite only</div>
+ }
}
return <div>Prometheus</div>
}
(here are the files diff-free)
// File: features/Prometheus/Prometheus.tsx
// React component version
function Prometheus() {
return (
<IsFeatureEnabled
feature="prometheus"
ifDisabled={(reasons: { doNotMatch }) => {
if (doNotMatch.eeLite) {
return <div>Prometheus is enabled for EE Lite only</div>
}
}}
>
<div>Prometheus</div>
</IsFeatureEnabled>
);
}
// React hook version
function Prometheus() {
const {
status,
reasons: { doNotMatch }
} = useIsFeatureEnabled('prometheus');
if(status === 'disabled') {
if (doNotMatch.eeLite) {
return <div>Prometheus is enabled for EE Lite only</div>
}
}
return <div>Prometheus</div>
}
And also, I want to know the current type the Console is running.
// File: features/Prometheus/Prometheus.tsx
// React component version
function Prometheus() {
return (
<IsFeatureEnabled
feature="prometheus"
- ifDisabled={(reasons: { doNotMatch }) => {
+ ifDisabled={(reasons: { doNotMatch }, current: { hasuraPlan }) => {
if (doNotMatch.eeLite) {
+ if(hasuraPlan.type === 'ce') {
+ return <div>Try EE Lite and give all the paid feature a try for free!</div>
+ }
+
return <div>Prometheus is enabled for EE Lite only</div>
}
}}
>
<div>Prometheus</div>
</IsFeatureEnabled>
);
}
// React hook version
function Prometheus() {
const {
status,
reasons: { doNotMatch }
+ current: { hasuraPlan }
} = useIsFeatureEnabled('prometheus');
if(status === 'disabled') {
if (doNotMatch.eeLite) {
+ if(hasuraPlan.type === 'ce') {
+ return <div>Try EE Lite and give all the paid feature a try for free!</div>
+ }
+
return <div>Prometheus is enabled for EE Lite only</div>
}
}
return <div>Prometheus</div>
}
(here are the files diff-free)
// File: features/Prometheus/Prometheus.tsx
// React component version
function Prometheus() {
return (
<IsFeatureEnabled
feature="prometheus"
ifDisabled={(reasons: { doNotMatch }, current: { hasuraPlan }) => {
if (doNotMatch.eeLite) {
if(hasuraPlan.type === 'ce') {
return <div>Try EE Lite and give all the paid feature a try for free!</div>
}
return <div>Prometheus is enabled for EE Lite only</div>
}
}}
>
<div>Prometheus</div>
</IsFeatureEnabled>
);
}
// React hook version
function Prometheus() {
const {
status,
reasons: { doNotMatch }
current: { hasuraPlan }
} = useIsFeatureEnabled('prometheus');
if(status === 'disabled') {
if (doNotMatch.eeLite) {
if(hasuraPlan.type === 'ce') {
return <div>Try EE Lite and give all the paid feature a try for free!</div>
}
return <div>Prometheus is enabled for EE Lite only</div>
}
}
return <div>Prometheus</div>
}
// File: features/Neon/ForceRefetchLuxEntitlements.tsx
function ForceRefetchLuxEntitlements() {
const refetchLuxEntitlements = useRefetchLuxEntitlements();
return <button onClick={refetchLuxEntitlements}>Refetch Lux Entitlements</button>
}
π As a developer, I want to be sure my component is not rendered until the Hasura Plan data is available because I do not want to manage the loading state
The whole Console is not rendered until all the data is available, there is no need to manage loading states.
This POC does not include any Storybook APIs but we will implement:
- Some vertical components (for instance
<SimulateCloudConsole>
) that internally sets the store with the needed env vars and Hasura Plan data - A
<StorybookHasuraPlanControl>
that adds one more Storybook Control panel with the existing plugin @nicoinch implemented
π As a developer, I want to add one feature to the catalogue of features managed by useIsFeatureEnabled
(Neon, for instance)
// File: libs/hasura-features/src/lib/features.ts
const neon: CompatibilityObject = { // <-- new object
ce: 'disabled',
cliMode: 'cliOrServer',
cloud: 'enabled',
selfHostedCloud: 'disabled',
luxEntitlements: {
NeonDatabaseIntegration: 'required',
DatadogIntegration: 'notRequired',
},
eeLite: 'disabled',
eeLiteLicense: 'notRequired',
};
export const features: Record<string, CompatibilityObject> = {
prometheus,
neon, // <-- the feature is added to the list of supported features
};
See the features.ts file.
Add one more function to useLoadHasuraPlan
, like the existing useFetchLuxEntitlements
and useFetchEELiteLicense
examples.
See the useLoadHasuraPlan.ts file.
(This guide will be prepared for the final version of the library.)
A personal opinion: we should not have dynamic routes at all. Our customers know the product and hear about its feature here and there, I do not see value in showing "404" if the users navigate to <console>/settings/prometheus
in CE. I'd prefer, instead, to show our users a dedicated message for every version of the Console:
- Are the customers in CE? Let's tell them "Sorry, the feature is available only in EE Lite!"
- Are the customers in EE Lite without license? Let's tell them "Do you want to try EE license??"
- Are the customers in Cloud? Let's tell them "Go to the Cloud dashboard and set everything Prometheus"
- The
useIsFeatureEnabled
/<IsFeatureEnabled />
APIs: used maybe 95% of the times - The
features.ts
module: used maybe 5% of the times, every time we need to add a new feature or get an existing feature controlled by theuseIsFeatureEnabled
/<IsFeatureEnabled />
APIs - The
compatibility.ts
/store.ts
modules: used maybe 1% of the times, only when we need to add one more async source Hasura plan info
How the new APIs work under the hood?
- First of all, the application must create all the TanStack queries for all the dynamic data of the server, like the Lux entitlements, the EE Trial license details, etc.
- Based on the env vars received from the server, some of the above queries are run (to avoid trying to load the Lux entitlements when in EE Lite, for instance)
- When all the async data is received, the app can be rendered
(please note that in the real Console, some of the async data will be fetched/refetched after the authentication steps)
- From now on, the
useIsFeatureEnabled
can tell if a feature is enabled or not (and why) thanks to a list of features useIsFeatureEnabled
is reactive, so for instance when an EE trial license is activated,useIsFeatureEnabled
makes the React components consuming it re-render
I see Rect APIs, but I do not see pure JavaScript APIs, why?
React APIs includes "reactivity" by definition. Vanilla JavaScript APIs cannot offer the same reactivity in an easy way. If you need to consume the Hasura plan data offered by useIsFeatureEnabled
, read it from a React component and pass it down to your vanilla JavaScript functions.
How will I be able to access window.__env
if the goal of this POC is also to stop accessing it?
We will maybe expose a useEnvVars_UNSECURE
hook and we will look at when/where is needed.
I do not see anything about pricing plan, authentication, etc. in this POC, why?
Because the goal of this POC is not to reproduce every Console case but to validate an idea to manage them.
How can I explore the small codebase of this POC? What are the key parts?
From the high-level consumers to the low-level functions:
- Prometheus.tsx: a fake Prometheus feature that shows how the
useIsFeatureEnabled
can be used. - Neon.tsx: a fake Neon feature that shows how the
IsFeatureEnabled
component can be used. - useLoadHasuraPlan.ts: a hook to load all the dynamic data of the Hasura plan. The final one for the Console could be very similar to it.
- features.ts: this module will include all the features that depend on some details of the current Hasura plan.
- compatibility.spec.ts: allows to simulate all the different case managed by
checkFeatureCompatibility
, the function at the core ofuseIsFeatureEnabled
.
During the first round of presentations, we gathered the following feedback:
(by @mattheweric, @vijayprasanna13, @wawhal) What about getting the APIs accepting an array of features instead of just one?
We wil evaluate if the feature is really needed because @beaussan prepared some TypeScript magics to be sure that the doNotMatch
object contains only the needed properties to check at the TypeScript level. You can see it in action in the following screenshot. The goal is to avoid the developer caring about impossible situations (for instance, Neon cannot result in having to deal doNotMatch.eeLite
because Neon is not related to EE Lite at all).
Managing this Types at the array level is hard, that's why we prefer to stick for the single feature as of today.
(by @vijayprasanna13) What about specifying only the required properties in the compatibility object? For instance, to avoid specifying if Neon is enabled or not in EE Lite since it does not make sense.
Getting all the properties explicit enforces managing them also when we add more Console types/mode and source of info. It's a by-design choice.
(by @lucarestagno) Why calling it cliMode
instead of mode
?
The current APIs are temporary but this is a good point to fix in the final implementation.
(by @lucarestagno) What about feature flags?
They will be managed too in the final implementation.
(by @beaussan, @vijayprasanna13) The cloud
property of the compatibility object should not be separated from the Lux entitlements because they are strictly coupled
Indeed, I will change the final implementation.
Run nx serve feature-first-hasura-console-poc
for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
5-min intro
This is mostly for who needs to present the POC.
The mess with the env vars and asynchronous source of data that impacts showing a feature or not is high, and it will increase.
We (the frontenders of the platform team) thought about it, and we followed Rishi's proposal for some feature-first APIs that ease showing/hiding a feature (and why if needed) and took ownership of it, removing the burden from the feature teams.
More:
- We also want to deal with the "business names" of the things vs the code names of them
- We expose React-only APIs because the Hasura plan details can change during the app life
- We want to ease adding new features
- We want to create a centralized loader system for whatever Hasura plan (EE Lite license, Lux entitlements, Cloud pricing plans, etc.)
We then created a POC to share the API proposal with the ones who requested us more info about the problem and/or proposed some solutions.
We must gather feedback to validate the POC and quickly iterate on the following steps.
This is the proposal
- A React hook and a React component for the basic show/hide a feature
- Either of them can be used to know why a feature is not enabled
- Either of them can be used to know the current info of the Hasura plan
- Add a new feature
Do you have feedback?