Skip to content

NoriSte/feature-first-hasura-console-poc

Repository files navigation

Feature-First Hasura Console API POC

Context
  1. 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.
  2. 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).
  3. There are additional things to consider that are retrieved dynamically
    1. Lux entitlements
    2. Pricing tiers
    3. EE Trial license
    4. ... 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
  1. Through the proConsole module where we have been hidden checking the console type/mode in the last six months.
  2. 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

  1. Centralizing manage all the data/info/properties/vars that impact what the Console shows
  2. Allow this centralized management to scale for future needs
  3. Hide all the mess implementation details of dealing with the window.__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)

What to expect from this POC

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!

Basic use cases

πŸ™‹ As a developer, I want to show a feature only if it's enabled

// 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>
}

πŸ™‹ As a developer, I want to refresh some data related to the Hasura Plan

// 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.

πŸ™‹ As a developer, I want to test my component in all its versions in Storybook

This POC does not include any Storybook APIs but we will implement:

  1. Some vertical components (for instance <SimulateCloudConsole>) that internally sets the store with the needed env vars and Hasura Plan data
  2. A <StorybookHasuraPlanControl> that adds one more Storybook Control panel with the existing plugin @nicoinch implemented

Advanced use cases

πŸ™‹ 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.

πŸ™‹ As a developer, I want to add one more async source of Hasura Plan info

Add one more function to useLoadHasuraPlan, like the existing useFetchLuxEntitlements and useFetchEELiteLicense examples.

See the useLoadHasuraPlan.ts file.

πŸ™‹ As a developer, I want to add one more Console type

(This guide will be prepared for the final version of the library.)

πŸ™‹ As a developer, I want to manage the dynamic route for my feature

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:

  1. Are the customers in CE? Let's tell them "Sorry, the feature is available only in EE Lite!"
  2. Are the customers in EE Lite without license? Let's tell them "Do you want to try EE license??"
  3. Are the customers in Cloud? Let's tell them "Go to the Cloud dashboard and set everything Prometheus"

Three levels of abstraction

  1. The useIsFeatureEnabled/<IsFeatureEnabled /> APIs: used maybe 95% of the times
  2. 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 the useIsFeatureEnabled/<IsFeatureEnabled /> APIs
  3. 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

FAQ

How the new APIs work under the hood?

  1. 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.
  2. 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)
  3. 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)

  1. From now on, the useIsFeatureEnabled can tell if a feature is enabled or not (and why) thanks to a list of features
  2. 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:

  1. Prometheus.tsx: a fake Prometheus feature that shows how the useIsFeatureEnabled can be used.
  2. Neon.tsx: a fake Neon feature that shows how the IsFeatureEnabled component can be used.
  3. 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.
  4. features.ts: this module will include all the features that depend on some details of the current Hasura plan.
  5. compatibility.spec.ts: allows to simulate all the different case managed by checkFeatureCompatibility, the function at the core of useIsFeatureEnabled.

Feedback

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).

doNotMatch object and TypeScript

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.

How can I play with the demo?

POC screenshot

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:

  1. We also want to deal with the "business names" of the things vs the code names of them
  2. We expose React-only APIs because the Hasura plan details can change during the app life
  3. We want to ease adding new features
  4. 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

  1. A React hook and a React component for the basic show/hide a feature
  2. Either of them can be used to know why a feature is not enabled
  3. Either of them can be used to know the current info of the Hasura plan
  4. Add a new feature

Do you have feedback?

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published