Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ArchivedRecommendationsDetailsContainer from "containers/ArchivedRecommen
import RangePickerFormContainer from "containers/RangePickerFormContainer";
import { RECOMMENDATIONS } from "urls";
import { isEmptyArray } from "utils/arrays";
import { DATE_RANGE_TYPE, DOWNLOAD_FEATURE_ENABLED } from "utils/constants";
import { DATE_RANGE_TYPE } from "utils/constants";
import { SPACING_2 } from "utils/layouts";
import { isEmptyObject } from "utils/objects";

Expand Down Expand Up @@ -80,8 +80,7 @@ const ArchivedRecommendations = ({
type: "button",
action: onDownload,
isLoading: isDownloading,
dataTestId: "btn_download",
show: DOWNLOAD_FEATURE_ENABLED
dataTestId: "btn_download"
}
]
};
Expand Down
5 changes: 3 additions & 2 deletions ngui/ui/src/components/HeaderButtons/HeaderButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import { PRODUCT_TOUR, useStartTour } from "components/Tour";
import { useIsTourAvailableForCurrentBreakpoint } from "components/Tour/hooks";
import ProfileMenuContainer from "containers/ProfileMenuContainer";
import { useCommunityDocsContext } from "contexts/CommunityDocsContext";
import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled";
import { useMainMenuState } from "hooks/useMainMenuState";
import { useOrganizationInfo } from "hooks/useOrganizationInfo";
import { DOCS_HYSTAX_OPTSCALE } from "urls";
import { ENABLE_PRODUCT_TOUR } from "utils/constants";
import { MPT_BRAND_TYPE } from "../../utils/layouts";
import useStyles from "./HeaderButtons.styles";

const HeaderButtons = ({ isProductTourAvailable = false }) => {
const isProductTourEnabled = useIsFeatureEnabled("product_tour");
const { organizationId } = useOrganizationInfo();
const startTour = useStartTour();
const { classes } = useStyles();
Expand Down Expand Up @@ -58,7 +59,7 @@ const HeaderButtons = ({ isProductTourAvailable = false }) => {
value: <FormattedMessage id="documentation" />
}}
/>
{ENABLE_PRODUCT_TOUR && organizationId && (
{isProductTourEnabled && organizationId && (
<IconButton
dataTestId="btn_product_tour"
color="info"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel";
import EditOrganizationCurrencyFormContainer from "containers/EditOrganizationCurrencyFormContainer";
import { useAllDataSources } from "hooks/coreData/useAllDataSources";
import { useIsAllowed } from "hooks/useAllowedActions";
import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled";
import { useOrganizationInfo } from "hooks/useOrganizationInfo";
import { ENVIRONMENT, ORGANIZATION_EDIT_ALLOWED } from "utils/constants";
import { ENVIRONMENT } from "utils/constants";

const OrganizationCurrency = () => {
const { currency: currencyCode } = useOrganizationInfo();
Expand All @@ -18,8 +19,8 @@ const OrganizationCurrency = () => {
const [isEditMode, setIsEditMode] = useState(false);
const enableEditMode = () => setIsEditMode(true);
const disableEditMode = () => setIsEditMode(false);

const isEditAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }) && ORGANIZATION_EDIT_ALLOWED;
const isOrganizationEditAllowed = useIsFeatureEnabled("organization_edit_allowed");
const isEditAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }) && isOrganizationEditAllowed;

return isEditMode ? (
<EditOrganizationCurrencyFormContainer onCancel={disableEditMode} onSuccess={disableEditMode} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel";
import Tooltip from "components/Tooltip";
import EditOrganizationFormContainer from "containers/EditOrganizationFormContainer";
import { useIsAllowed } from "hooks/useAllowedActions";
import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled";
import { useOrganizationInfo } from "hooks/useOrganizationInfo";
import { OPTSCALE_CAPABILITY, ORGANIZATION_EDIT_ALLOWED } from "utils/constants";
import { OPTSCALE_CAPABILITY } from "utils/constants";
import { SPACING_1 } from "utils/layouts";
import { sliceByLimitWithEllipsis } from "utils/strings";
import OrganizationCurrency from "./OrganizationCurrency";
Expand Down Expand Up @@ -48,8 +49,8 @@ const OrganizationName = ({ name }: OrganizationNameProps) => {
const [isEditMode, setIsEditMode] = useState(false);
const enableEditMode = () => setIsEditMode(true);
const disableEditMode = () => setIsEditMode(false);

const isEditAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }) && ORGANIZATION_EDIT_ALLOWED;
const isOrganizationEditAllowed = useIsFeatureEnabled("organization_edit_allowed");
const isEditAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }) && isOrganizationEditAllowed;

if (isEditMode) {
return <EditOrganizationFormContainer onCancel={disableEditMode} onSuccess={disableEditMode} />;
Expand Down Expand Up @@ -85,6 +86,7 @@ const OrganizationName = ({ name }: OrganizationNameProps) => {

const OrganizationInfoSetting = () => {
const { name: organizationName, organizationId, currency } = useOrganizationInfo();
const isOrganizationEditAllowed = useIsFeatureEnabled("organization_edit_allowed");

return (
<Stack spacing={SPACING_1}>
Expand All @@ -95,7 +97,7 @@ const OrganizationInfoSetting = () => {
<OrganizationName name={organizationName} />
</Box>
<CapabilityWrapper capability={OPTSCALE_CAPABILITY.FINOPS}>
{ORGANIZATION_EDIT_ALLOWED && (
{isOrganizationEditAllowed && (
<Box>
<Typography>
<FormattedMessage id="organizationCurrencyDescription" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useDownloadCleanupScript } from "hooks/useDownloadCleanupScript";
import { useDownloadRecommendationItems } from "hooks/useDownloadRecommendationItems";
import { useOpenSideModal } from "hooks/useOpenSideModal";
import { DOCS_HYSTAX_CLEANUP_SCRIPTS } from "urls";
import { DOWNLOAD_FEATURE_ENABLED, DOWNLOAD_FILE_FORMATS, SCOPE_TYPES } from "utils/constants";
import { DOWNLOAD_FILE_FORMATS, SCOPE_TYPES } from "utils/constants";
import { isEmptyObject } from "utils/objects";
import RecommendationDetailsService from "../RecommendationDetailsService";

Expand Down Expand Up @@ -49,7 +49,7 @@ const useActionBarItems = ({ downloadLimit, recommendation, dataSourceIds, withD
dataTestId: "download",
isLoading: isDownloadLoading,
disabled: !hasItems,
show: DOWNLOAD_FEATURE_ENABLED && withDownload,
show: withDownload,
menu: {
items: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ButtonLoader from "components/ButtonLoader";
import Invitations from "components/Invitations";
import { Error, Loading } from "containers/InitializeContainer/common";
import { useOrganizationsQuery } from "graphql/__generated__/hooks/restapi";
import { ALLOW_ORGANIZATION_CREATION } from "utils/constants";
import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled";
import { SPACING_1, SPACING_2 } from "utils/layouts";

const useStyles = makeStyles()((theme) => ({
Expand All @@ -25,6 +25,7 @@ const useStyles = makeStyles()((theme) => ({

const AcceptInvitations = ({ invitations, refetchInvitations, onProceed }) => {
const { classes } = useStyles();
const isOrganizationCreationAllowed = useIsFeatureEnabled("organization_creation_allowed");

const {
data,
Expand All @@ -48,7 +49,7 @@ const AcceptInvitations = ({ invitations, refetchInvitations, onProceed }) => {
}

const userHasOrganizations = data && data.organizations && data.organizations.length > 0;
const redirectToPendingInvitations = !ALLOW_ORGANIZATION_CREATION && !userHasOrganizations;
const redirectToPendingInvitations = !isOrganizationCreationAllowed && !userHasOrganizations;

if (redirectToPendingInvitations) {
return onProceed();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState } from "react";
import { NetworkStatus } from "@apollo/client";
import { useInvitationsQuery } from "graphql/__generated__/hooks/restapi";
import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled";
import { isEmptyArray } from "utils/arrays";
import { ALLOW_ORGANIZATION_CREATION } from "utils/constants";
import { Error, Loading } from "../../common";
import ProceedToApplication from "../ProceedToApplication";
import SetupOrganization from "../SetupOrganization/StepContainer";
Expand All @@ -19,6 +19,7 @@ const StepContainer = () => {
fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true
});
const isOrganizationCreationAllowed = useIsFeatureEnabled("organization_creation_allowed");

const onRefetch = ({ onSuccess, onError } = {}) => {
refetchInvitations()
Expand Down Expand Up @@ -51,7 +52,7 @@ const StepContainer = () => {
}

if (proceedToNext) {
return ALLOW_ORGANIZATION_CREATION ? (
return isOrganizationCreationAllowed ? (
<SetupOrganization isInvitationsRefetching={getInvitationsRefetching} refetchInvitations={onRefetch} />
) : (
<ProceedToApplication />
Expand All @@ -72,7 +73,7 @@ const StepContainer = () => {
);
}

return ALLOW_ORGANIZATION_CREATION ? (
return isOrganizationCreationAllowed ? (
<SetupOrganization isInvitationsRefetching={getInvitationsRefetching} refetchInvitations={onRefetch} />
) : (
<ProceedToApplication />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import IconButton from "components/IconButton";
import MailTo from "components/MailTo";
import Tooltip from "components/Tooltip";
import { DOCS_HYSTAX_CLEANUP_SCRIPTS, EMAIL_SALES } from "urls";
import { DOWNLOAD_FEATURE_ENABLED } from "utils/constants";
import { Menu } from "../RecommendationCard";
import { useDownloadCleanupScripts, useDownloadItems, usePinItems, useSettingItems } from "./hooks";

Expand Down Expand Up @@ -93,7 +92,7 @@ const Actions = ({
return (
<Box display="flex">
{withCleanupScripts && hasItems && <DownloadCleanupScripts recommendation={recommendation} />}
{DOWNLOAD_FEATURE_ENABLED && hasItems && (
{hasItems && (
<DownloadItems
recommendation={recommendation}
downloadLimit={downloadLimit}
Expand Down
6 changes: 6 additions & 0 deletions ngui/ui/src/hooks/useIsFeatureEnabled.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { themeFeature } from "../utils/features/themeFeature";
import { useOrganizationFeatures } from "./coreData/useOrganizationFeatures";

export const useIsFeatureEnabled = (featureName: string) => {
const { [featureName]: featureFlag = 0 } = useOrganizationFeatures();
const themeFeatureFlag = themeFeature(featureName);

if (themeFeatureFlag !== undefined) {
return themeFeatureFlag;
}

return featureFlag === 1;
};
11 changes: 11 additions & 0 deletions ngui/ui/src/theme.features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type FeatureFlags = {
[key: string]: boolean | undefined;
};

export const featureFlags: FeatureFlags = {
organization_creation_allowed: false,
organization_edit_allowed: false,
product_tour: false,
paid_organization: true,
kubernetes_data_source: false
};
5 changes: 0 additions & 5 deletions ngui/ui/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,8 +1061,3 @@ export const EMPTY_BREAKDOWN_KEY = Object.freeze({
NOT_SET: "(not set)",
NULL: "null"
});

export const ALLOW_ORGANIZATION_CREATION = false;
export const ENABLE_PRODUCT_TOUR = false;
export const ORGANIZATION_EDIT_ALLOWED = false;
export const DOWNLOAD_FEATURE_ENABLED = false;
59 changes: 59 additions & 0 deletions ngui/ui/src/utils/features/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Features

In this directory you can find *feature flag* related utils

## Theme Features

Theme features are created to be able to control some of the features per theme. They take precedence over the API Organization Features (look here: ngui/ui/src/hooks/useIsFeatureEnabled.ts)

All the flags are kept in *ngui/ui/src/theme.feature.ts* file to keep this configuration as close as possible to theme. We keep snake_case naming convention to follow naming conventions used by feature flags API.

Flag can have tree values:
true - permanently enable
false - permanently disabled
undefined - respect API organization flag

### Examples

#### Example 1. paid_organization

We do not want to have any feaures to be demo features or disabled if someone does not pay for the platform. We control access to the platform so all features are enabled by default.

This is why we set the flag __paid_organization__ in *ngui/ui/src/theme.feature.ts* to permanently enable it in the whole platform where useIsPaidOrganization hook is used.

```
const isPaidOrganization = useIsPaidOrganization();
```

#### Example 2. Hide product tour

We want to hide product tour for all users

```
const isProductTourEnabled = useIsFeatureEnabled("product_tour");

...

{isProductTourEnabled && organizationId && (
<IconButton
dataTestId="btn_product_tour"
color="info"
icon={<LiveHelpOutlinedIcon />}
onClick={startProductTour}
disabled={!isProductTourAvailable || !isTourAvailableForCurrentBreakpoint}
tooltip={{
show: true,
value: <FormattedMessage id="productTour" />
}}
/>
)}

```

## Next steps

We can introduce another level of Feature Flags (environment variable, runtime controled flags)

## Notes

This feature is important for themification.
7 changes: 7 additions & 0 deletions ngui/ui/src/utils/features/themeFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { featureFlags } from "../../theme.features";

export const themeFeature = (featureName: string): boolean | undefined => {
const { [featureName]: featureFlag } = featureFlags;

return featureFlag;
};