Skip to content

Commit 14af803

Browse files
authored
Merge pull request #1991 from visualize-admin/feat/flags
feat: Hide new features behind flags + remove depreciated flags automatically
2 parents 77f6d81 + dd26506 commit 14af803

12 files changed

+148
-57
lines changed

CHANGELOG.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ You can also check the
1212
# Unreleased
1313

1414
- Features
15-
- Added a way to add text objects to dashboard layouts
16-
- Added Markdown support in chart titles and descriptions
15+
- Added a way to add text objects to dashboard layouts (hidden behind an
16+
enable-experimental-features flag)
17+
- Added Markdown support in chart titles and descriptions (hidden behind an
18+
enable-experimental-features flag)
1719
- Centered x-axis labels
1820
- Added y-axis labels
1921
- Added a new option to the vertical axis on the line chart, allowing users to
2022
display a dot over the line where it intercepts the x-axis ticks
2123
- Downloading images of bar charts now includes the whole chart, not just the
2224
visible part
25+
- Bar charts are now hidden behind a an enable-experimental-features flag
2326
- Fixes
2427
- Preview via API now works correctly for map charts
2528
- GraphQL debug panel now displays the queries correctly when in debug mode

app/components/markdown.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import rehypeRaw from "rehype-raw";
44
import rehypeSanitize from "rehype-sanitize";
55
import remarkGfm from "remark-gfm";
66

7+
import { useFlag } from "@/flags";
8+
79
const components: ComponentProps<typeof ReactMarkdown>["components"] = {
810
h1: ({ children, style, ...props }) => (
911
<h1 style={{ ...style, marginTop: 0 }} {...props}>
@@ -50,12 +52,16 @@ const components: ComponentProps<typeof ReactMarkdown>["components"] = {
5052
export const Markdown = (
5153
props: Omit<ComponentProps<typeof ReactMarkdown>, "components">
5254
) => {
53-
return (
55+
const enable = useFlag("enable-experimental-features");
56+
57+
return enable ? (
5458
<ReactMarkdown
5559
components={components}
5660
remarkPlugins={[remarkGfm]}
5761
rehypePlugins={[rehypeRaw, rehypeSanitize]}
5862
{...props}
5963
/>
64+
) : (
65+
<>{props.children}</>
6066
);
6167
};

app/configurator/components/chart-type-selector.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ControlSectionSkeleton } from "@/configurator/components/chart-controls
1717
import { IconButton } from "@/configurator/components/icon-button";
1818
import { useAddOrEditChartType } from "@/configurator/config-form";
1919
import { ConfiguratorStateWithChartConfigs } from "@/configurator/configurator-state";
20+
import { useFlag } from "@/flags";
2021
import { useDataCubesComponentsQuery } from "@/graphql/hooks";
2122
import { useLocale } from "@/locales/use-locale";
2223

@@ -39,6 +40,7 @@ export const ChartTypeSelector = (props: ChartTypeSelectorProps) => {
3940
chartKey,
4041
...rest
4142
} = props;
43+
const enableBarChart = useFlag("enable-experimental-features");
4244
const locale = useLocale();
4345
const chartConfig = getChartConfig(state);
4446
const [{ data }] = useDataCubesComponentsQuery({
@@ -121,7 +123,11 @@ export const ChartTypeSelector = (props: ChartTypeSelectorProps) => {
121123
message: "Regular",
122124
})}
123125
currentChartType={chartType}
124-
chartTypes={regularChartTypes}
126+
chartTypes={
127+
enableBarChart
128+
? regularChartTypes
129+
: regularChartTypes.filter((chartType) => chartType !== "bar")
130+
}
125131
possibleChartTypesDict={possibleChartTypesDict}
126132
onClick={handleClick}
127133
testId="chart-type-selector-regular"

app/configurator/components/field.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
isMostRecentValue,
9191
VISUALIZE_MOST_RECENT_VALUE,
9292
} from "@/domain/most-recent-value";
93+
import { useFlag } from "@/flags";
9394
import { useTimeFormatLocale } from "@/formatters";
9495
import { TimeUnit } from "@/graphql/query-hooks";
9596
import { Locale } from "@/locales/locales";
@@ -680,12 +681,17 @@ export const MetaInputField = ({
680681
value?: string;
681682
}) => {
682683
const field = useMetaField({ type, metaKey, locale, value });
684+
const enableMarkdown = useFlag("enable-experimental-features");
683685

684686
switch (inputType) {
685687
case "text":
686688
return <Input label={label} {...field} />;
687689
case "markdown":
688-
return <MarkdownInput label={label} {...field} />;
690+
if (enableMarkdown) {
691+
return <MarkdownInput label={label} {...field} />;
692+
} else {
693+
return <Input label={label} {...field} />;
694+
}
689695
}
690696
};
691697

app/configurator/components/layout-configurator.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
TemporalDimension,
6666
TemporalEntityDimension,
6767
} from "@/domain/data";
68+
import { useFlag } from "@/flags";
6869
import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters";
6970
import { useConfigsCubeComponents } from "@/graphql/hooks";
7071
import { Icon } from "@/icons";
@@ -565,6 +566,12 @@ const LayoutBlocksConfigurator = () => {
565566
dispatch({ type: "LAYOUT_ACTIVE_FIELD_CHANGED", value: blockKey });
566567
});
567568

569+
const enabled = useFlag("enable-experimental-features");
570+
571+
if (!enabled) {
572+
return null;
573+
}
574+
568575
return layout.type === "dashboard" ? (
569576
<ControlSection role="tablist" aria-labelledby="controls-blocks" collapse>
570577
<SubsectionTitle titleId="controls-blocks" gutterBottom={false}>

app/flags/flag.tsx

+17-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import qs from "qs";
33
import { isRunningInBrowser } from "@/utils/is-running-in-browser";
44

55
import FlagStore from "./store";
6-
import { FlagName, FlagValue } from "./types";
6+
import { FlagName, FLAGS, FlagValue } from "./types";
77

8-
const FLAG_PREFIX = "flag__";
8+
export const FLAG_PREFIX = "flag__";
99

1010
const store = new FlagStore();
1111

@@ -18,24 +18,34 @@ export const flag = function flag(...args: [FlagName] | [FlagName, FlagValue]) {
1818
} else {
1919
const [name, value] = args;
2020
store.set(name, value);
21+
2122
return value;
2223
}
2324
};
2425

26+
/** List all flag names from the store */
27+
const listFlagNames = () => {
28+
return store.keys().sort();
29+
};
30+
2531
/** List all flags from the store */
2632
const listFlags = () => {
27-
return store.keys().sort();
33+
return listFlagNames().map((name) => ({
34+
name,
35+
description: FLAGS.find((flag) => flag.name === name)?.description,
36+
value: store.get(name as FlagName),
37+
}));
2838
};
2939

3040
/** Resets all the flags */
3141
const resetFlags = () => {
32-
listFlags().forEach((name) => store.remove(name as FlagName));
42+
listFlagNames().forEach((name) => store.remove(name as FlagName));
3343
};
3444

3545
/**
3646
* Enables several flags
3747
*
38-
* Supports passing either object flagName -> flagValue
48+
* Supports passing either object flagName -> flagValue
3949
*
4050
* @param {Record<string, boolean>} flagsToEnable
4151
*/
@@ -52,6 +62,7 @@ const enable = (flagsToEnable: FlagName[]) => {
5262
};
5363

5464
flag.store = store;
65+
flag.listNames = listFlagNames;
5566
flag.list = listFlags;
5667
flag.reset = resetFlags;
5768
flag.enable = enable;
@@ -61,6 +72,7 @@ const initFromSearchParams = (locationSearch: string) => {
6172
? locationSearch.substr(1)
6273
: locationSearch;
6374
const params = qs.parse(locationSearch);
75+
6476
for (const [param, value] of Object.entries(params)) {
6577
if (param.startsWith(FLAG_PREFIX) && typeof value === "string") {
6678
try {

app/flags/ls-adapter.ts app/flags/local-storage-adapter.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { FlagName, FlagValue } from "./types";
1+
import { FLAG_PREFIX } from "@/flags/flag";
22

3-
const prefix = "flag__";
3+
import { FlagName, FlagValue } from "./types";
44

55
type FlagNameOrString = FlagName | (string & {});
66

7-
const getKey = (name: FlagNameOrString) => prefix + name;
7+
const getKey = (name: FlagNameOrString) => `${FLAG_PREFIX}${name}`;
88

99
const listFlagLocalStorage = () => {
1010
return Object.keys(localStorage)
11-
.filter((x) => x.indexOf(prefix) === 0)
12-
.map((x) => x.replace(prefix, ""));
11+
.filter((x) => x.indexOf(FLAG_PREFIX) === 0)
12+
.map((x) => x.replace(FLAG_PREFIX, ""));
1313
};
1414

1515
/**

app/flags/store.ts

+32-18
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,52 @@
11
import mitt, { Emitter } from "mitt";
22

3-
import lsAdapter from "./ls-adapter";
4-
import { FlagName, FlagValue } from "./types";
3+
import localStorageAdapter from "./local-storage-adapter";
4+
import { FlagName, FlagValue, FLAG_NAMES } from "./types";
55

6-
type Events = { change: string };
6+
type Events = {
7+
change: string;
8+
};
79

810
/**
911
* In memory key value storage.
1012
*
11-
* Can potentially be backed by localStorage if present
13+
* Can potentially be backed by localStorage if present.
1214
13-
* Emits `change` when a key is set (eventEmitter)
15+
* Emits `change` when a key is set (eventEmitter).
1416
*/
1517
class FlagStore {
16-
longtermStore: typeof lsAdapter | null;
17-
18+
longTermStore: typeof localStorageAdapter | null;
1819
store: Record<string, any>;
1920
ee: Emitter<Events>;
2021

2122
constructor() {
2223
this.store = {};
23-
this.longtermStore = null;
24+
this.longTermStore = null;
2425
this.ee = mitt();
26+
2527
if (typeof localStorage !== "undefined") {
26-
this.longtermStore = lsAdapter;
28+
this.longTermStore = localStorageAdapter;
2729
}
30+
2831
this.restore();
2932
}
3033

3134
restore() {
32-
if (!this.longtermStore) {
35+
const longTermStore = this.longTermStore;
36+
37+
if (!longTermStore) {
3338
return;
3439
}
35-
const allValues = this.longtermStore.getAll();
40+
41+
const allValues = longTermStore.getAll();
42+
3643
Object.entries(allValues).forEach(([flag, val]) => {
37-
this.store[flag] = val;
38-
this.ee.emit("change", flag);
44+
if (FLAG_NAMES.includes(flag as FlagName)) {
45+
this.store[flag] = val;
46+
this.ee.emit("change", flag);
47+
} else {
48+
longTermStore.removeItem(flag);
49+
}
3950
});
4051
}
4152

@@ -47,26 +58,29 @@ class FlagStore {
4758
if (!Object.prototype.hasOwnProperty.call(this.store, name)) {
4859
this.store[name] = null;
4960
}
61+
5062
return this.store[name];
5163
}
5264

5365
set(name: FlagName, value: FlagValue) {
54-
if (this.longtermStore) {
55-
this.longtermStore.setItem(name, value);
66+
if (this.longTermStore) {
67+
this.longTermStore.setItem(name, value);
5668
}
69+
5770
this.store[name] = value;
5871
this.ee.emit("change", name);
5972
}
6073

6174
remove(name: FlagName) {
6275
delete this.store[name];
63-
if (this.longtermStore) {
64-
this.longtermStore.removeItem(name);
76+
77+
if (this.longTermStore) {
78+
this.longTermStore.removeItem(name);
6579
}
80+
6681
this.ee.emit("change", name);
6782
}
6883

69-
// eslint-disable-next-line class-methods-use-this
7084
removeListener(_event: string, _fn: (changed: string) => void) {}
7185
}
7286

app/flags/types.ts

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
export type FlagValue = any;
2-
3-
export type FlagName =
4-
/** Whether debug UI like the configurator debug panel is shown */
5-
| "debug"
6-
/** Whether server side cache is disabled */
7-
| "server-side-cache.disable"
8-
/** The GraphQL endpoint, is used for testing purposes */
9-
| "graphql.endpoint"
10-
/** Use at your own risk */
11-
| "easter-eggs";
2+
export const FLAGS = [
3+
{
4+
name: "debug" as const,
5+
description:
6+
"Controls whether debug elements are shown, e.g. ConfiguratorState viewer or GraphQL debug panel.",
7+
},
8+
{
9+
name: "server-side-cache.disable" as const,
10+
description: "Disables server side cache.",
11+
},
12+
{
13+
name: "graphql.endpoint" as const,
14+
description: "The GraphQL endpoint, can be used for testing purposes.",
15+
},
16+
{
17+
name: "easter-eggs" as const,
18+
description: "Enables easter eggs",
19+
},
20+
{
21+
name: "enable-experimental-features" as const,
22+
description:
23+
"Enables experimental features, including dashboard text blocks, Markdown editor and bar charts.",
24+
},
25+
];
26+
export const FLAG_NAMES = FLAGS.map((flag) => flag.name);
27+
type Flag = (typeof FLAGS)[number];
28+
export type FlagName = Flag["name"];

app/flags/useFlag.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,40 @@ import { useEffect, useState } from "react";
33
import { flag } from "./flag";
44
import { FlagName } from "./types";
55

6-
export default function useFlag(name: FlagName) {
6+
export default function useFlagValue(name: FlagName) {
77
const [flagValue, setFlag] = useState(() => flag(name));
8+
89
useEffect(() => {
910
const handleChange = (changed: string) => {
1011
if (changed === name) {
1112
setFlag(flag(name));
1213
}
1314
};
15+
1416
flag.store.ee.on("change", handleChange);
17+
1518
return () => {
1619
flag.store.removeListener("change", handleChange);
1720
};
1821
}, [setFlag, name]);
22+
1923
return flagValue;
2024
}
2125

2226
export function useFlags() {
2327
const [flags, setFlags] = useState(flag.list());
28+
2429
useEffect(() => {
2530
const handleChange = () => {
2631
setFlags(flag.list());
2732
};
33+
2834
flag.store.ee.on("change", handleChange);
35+
2936
return () => {
3037
flag.store.removeListener("change", handleChange);
3138
};
3239
}, [setFlags]);
33-
return flags as FlagName[];
40+
41+
return flags;
3442
}

0 commit comments

Comments
 (0)