From c9c46f1dc8689816f2e12776667d001e26e2f60a Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Wed, 23 Nov 2022 13:27:41 +0100 Subject: [PATCH 01/19] add context for customization settings --- .../turnilo-application.tsx | 12 +++++++ .../views/cube-view/settings-context.ts | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/client/views/cube-view/settings-context.ts diff --git a/src/client/applications/turnilo-application/turnilo-application.tsx b/src/client/applications/turnilo-application/turnilo-application.tsx index 693f57641..da2a7dda2 100644 --- a/src/client/applications/turnilo-application/turnilo-application.tsx +++ b/src/client/applications/turnilo-application/turnilo-application.tsx @@ -38,6 +38,9 @@ import { GeneralError } from "../../views/error-view/general-error"; import { HomeView } from "../../views/home-view/home-view"; import "./turnilo-application.scss"; import { cube, generalError, home, oauthCodeHandler, oauthMessageView, View } from "./view"; +import { SettingsContext, SettingsValue } from "../../views/cube-view/settings-context"; +import memoizeOne from "memoize-one"; +import { ClientCustomization } from "../../../common/models/customization/customization"; export interface TurniloApplicationProps { version: string; @@ -226,13 +229,22 @@ export class TurniloApplication extends React.Component ({ customization })); + render() { return
+ {this.renderView()} {this.renderAboutModal()} +
; } diff --git a/src/client/views/cube-view/settings-context.ts b/src/client/views/cube-view/settings-context.ts new file mode 100644 index 000000000..8327e6ed0 --- /dev/null +++ b/src/client/views/cube-view/settings-context.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2022 Allegro.pl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useContext } from "react"; +import { ClientCustomization } from "../../../common/models/customization/customization"; + +export interface SettingsValue { + customization: ClientCustomization; +} + +export const SettingsContext = React.createContext({ + get customization(): ClientCustomization { + throw new Error("Attempted to consume SettingsContext when there was no Provider in place."); + } +}); + +export function useSettingsContext(): SettingsValue { + return useContext(SettingsContext); +} From b902ec6ba56e5710af175c830caecb82f81f1472 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Wed, 23 Nov 2022 13:27:52 +0100 Subject: [PATCH 02/19] example how to use settings context --- src/client/components/header-bar/header-bar.tsx | 6 +++--- src/client/views/home-view/home-view.tsx | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/client/components/header-bar/header-bar.tsx b/src/client/components/header-bar/header-bar.tsx index c8776248b..f36584cde 100644 --- a/src/client/components/header-bar/header-bar.tsx +++ b/src/client/components/header-bar/header-bar.tsx @@ -16,16 +16,16 @@ */ import React from "react"; -import { ClientCustomization } from "../../../common/models/customization/customization"; +import { useSettingsContext } from "../../views/cube-view/settings-context"; import "./header-bar.scss"; export interface HeaderBarProps { - customization?: ClientCustomization; title?: string; } export const HeaderBar: React.FunctionComponent = props => { - const { customization, title } = props; + const { title } = props; + const { customization } = useSettingsContext(); const headerStyle: React.CSSProperties = customization && customization.headerBackground && { background: customization.headerBackground }; diff --git a/src/client/views/home-view/home-view.tsx b/src/client/views/home-view/home-view.tsx index be24bfca8..5cf1d297d 100644 --- a/src/client/views/home-view/home-view.tsx +++ b/src/client/views/home-view/home-view.tsx @@ -73,15 +73,12 @@ export class HomeView extends React.Component { } render() { - const { onOpenAbout, dataCubes, customization } = this.props; + const { onOpenAbout, dataCubes } = this.props; const { query } = this.state; const hasDataCubes = dataCubes.length > 0; return
- + From f3080e84533af77049d8a123da20c8ac30a28fa4 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Wed, 23 Nov 2022 13:48:52 +0100 Subject: [PATCH 03/19] add visualizationColors to customization settings --- src/client/deserializers/customization.ts | 5 +- .../app-settings/app-settings.fixtures.ts | 7 ++- src/common/models/colors/colors.ts | 25 +++++++++ .../customization/customization.fixtures.ts | 5 ++ .../customization/customization.mocha.ts | 52 ++++++++++++++++++- .../models/customization/customization.ts | 23 ++++++-- 6 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/client/deserializers/customization.ts b/src/client/deserializers/customization.ts index bf6064601..3d09dd1ff 100644 --- a/src/client/deserializers/customization.ts +++ b/src/client/deserializers/customization.ts @@ -19,7 +19,7 @@ import { ClientCustomization, SerializedCustomization } from "../../common/model import { deserialize as deserializeLocale } from "../../common/models/locale/locale"; export function deserialize(customization: SerializedCustomization): ClientCustomization { - const { headerBackground, messages, locale, customLogoSvg, timezones, externalViews, hasUrlShortener, sentryDSN } = customization; + const { headerBackground, messages, locale, customLogoSvg, timezones, externalViews, hasUrlShortener, sentryDSN, visualizationColors } = customization; return { headerBackground, customLogoSvg, @@ -28,6 +28,7 @@ export function deserialize(customization: SerializedCustomization): ClientCusto sentryDSN, messages, locale: deserializeLocale(locale), - timezones: timezones.map(Timezone.fromJS) + timezones: timezones.map(Timezone.fromJS), + visualizationColors }; } diff --git a/src/common/models/app-settings/app-settings.fixtures.ts b/src/common/models/app-settings/app-settings.fixtures.ts index 950bd1561..81d3f8b44 100644 --- a/src/common/models/app-settings/app-settings.fixtures.ts +++ b/src/common/models/app-settings/app-settings.fixtures.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { DEFAULT_COLORS } from "../colors/colors"; import { LOCALES } from "../locale/locale"; import { AppSettings, ClientAppSettings } from "./app-settings"; @@ -25,7 +26,8 @@ export const clientAppSettings: ClientAppSettings = { externalViews: [], timezones: [], locale: LOCALES["en-US"], - messages: {} + messages: {}, + visualizationColors: DEFAULT_COLORS }, oauth: { status: "disabled" }, clientTimeout: 1000 @@ -38,7 +40,8 @@ export const appSettings: AppSettings = { locale: LOCALES["en-US"], externalViews: [], cssVariables: {}, - messages: {} + messages: {}, + visualizationColors: DEFAULT_COLORS }, oauth: { status: "disabled" }, version: 0 diff --git a/src/common/models/colors/colors.ts b/src/common/models/colors/colors.ts index e4d2ded48..770f0ed70 100644 --- a/src/common/models/colors/colors.ts +++ b/src/common/models/colors/colors.ts @@ -27,3 +27,28 @@ export const NORMAL_COLORS = [ "#B0B510", "#904064" ]; + +export const DEFAULT_SERIES_COLORS = [ + "#2D95CA", + "#EFB925", + "#DA4E99", + "#4CC873", + "#745CBD", + "#EA7136", + "#E68EE0", + "#218C35", + "#B0B510", + "#904064" +]; + +export const DEFAULT_MAIN_COLOR = "#FF5900"; + +export const DEFAULT_COLORS: VisualizationColors = { + main: DEFAULT_MAIN_COLOR, + series: DEFAULT_SERIES_COLORS +} + +export interface VisualizationColors { + main: string; + series: Array; +} diff --git a/src/common/models/customization/customization.fixtures.ts b/src/common/models/customization/customization.fixtures.ts index 285e7e61d..0a94b2ec2 100644 --- a/src/common/models/customization/customization.fixtures.ts +++ b/src/common/models/customization/customization.fixtures.ts @@ -14,10 +14,15 @@ * limitations under the License. */ +import { DEFAULT_COLORS } from "../colors/colors"; import { UrlShortenerContext } from "../url-shortener/url-shortener"; import { Customization, CustomizationJS } from "./customization"; export const customization: Customization = { + messages: { + dataCubeNotFound: "404: Data Cube Not Found" + }, + visualizationColors: DEFAULT_COLORS, cssVariables: { "brand-selected": "orange", "brand": "red", diff --git a/src/common/models/customization/customization.mocha.ts b/src/common/models/customization/customization.mocha.ts index fad05f634..e0754d9bc 100644 --- a/src/common/models/customization/customization.mocha.ts +++ b/src/common/models/customization/customization.mocha.ts @@ -18,6 +18,7 @@ import { expect } from "chai"; import { Timezone } from "chronoshift"; import * as sinon from "sinon"; +import { DEFAULT_COLORS, DEFAULT_MAIN_COLOR, DEFAULT_SERIES_COLORS } from "../colors/colors"; import * as localeModule from "../locale/locale"; import * as urlShortenerModule from "../url-shortener/url-shortener"; import { Customization, DEFAULT_TIMEZONES, DEFAULT_TITLE, fromConfig, serialize } from "./customization"; @@ -161,6 +162,44 @@ describe("Customization", () => { }); }); }); + + describe("visualizationColors", () => { + it("should set default colors if no colors are defined", () => { + const customization = fromConfig({ ...customizationJS, visualizationColors: undefined }); + + expect(customization).to.deep.contain({ visualizationColors: DEFAULT_COLORS }); + }); + + it("should override default main color when property is defined", () => { + const customization = fromConfig({ + ...customizationJS, visualizationColors: { + main: "foobar-color" + } + }); + + expect(customization).to.deep.contain({ + visualizationColors: { + series: DEFAULT_SERIES_COLORS, + main: "foobar-color" + } + }); + }); + + it("should override default series colors when property is defined", () => { + const customization = fromConfig({ + ...customizationJS, visualizationColors: { + series: ["one fish", "two fish", "red fish", "blue fish"] + } + }); + + expect(customization).to.deep.contain({ + visualizationColors: { + series: ["one fish", "two fish", "red fish", "blue fish"], + main: DEFAULT_MAIN_COLOR + } + }); + }); + }); }); describe("serialize", () => { @@ -203,6 +242,16 @@ describe("Customization", () => { expect(serialized).to.deep.contain({ timezones: ["Europe/Warsaw", "Asia/Manila"] }); }); + it("should pass visualizationColors as is", () => { + const colors = { main: "fake-color", series: ["one series color"] }; + const serialized = serialize({ + ...customization, + visualizationColors: colors + }); + + expect(serialized).to.deep.contain({ visualizationColors: colors }); + }); + describe("externalViews", () => { // TODO: Implement }); @@ -212,7 +261,8 @@ describe("Customization", () => { it("should return hasUrlShortener true if has url shortener", () => { const serialized = serialize({ ...customization, - urlShortener: (() => {}) as any + urlShortener: (() => { + }) as any }); expect(serialized).to.contain({ hasUrlShortener: true }); diff --git a/src/common/models/customization/customization.ts b/src/common/models/customization/customization.ts index 1e9092399..dfeb32d84 100644 --- a/src/common/models/customization/customization.ts +++ b/src/common/models/customization/customization.ts @@ -18,7 +18,8 @@ import { Timezone } from "chronoshift"; import { LOGGER } from "../../logger/logger"; import { assoc } from "../../utils/functional/functional"; -import { isTruthy } from "../../utils/general/general"; +import { isNil, isTruthy } from "../../utils/general/general"; +import { DEFAULT_COLORS, DEFAULT_MAIN_COLOR, DEFAULT_SERIES_COLORS, VisualizationColors } from "../colors/colors"; import { ExternalView, ExternalViewValue } from "../external-view/external-view"; import { fromConfig as localeFromConfig, Locale, LocaleJS, serialize as serializeLocale } from "../locale/locale"; import { fromConfig as urlShortenerFromConfig, UrlShortener, UrlShortenerDef } from "../url-shortener/url-shortener"; @@ -120,6 +121,7 @@ export interface Customization { cssVariables: CssVariables; locale: Locale; messages: Messages; + visualizationColors: VisualizationColors; } export interface CustomizationJS { @@ -133,6 +135,7 @@ export interface CustomizationJS { sentryDSN?: string; cssVariables?: Record; messages?: Messages; + visualizationColors?: Partial; } export interface SerializedCustomization { @@ -144,6 +147,7 @@ export interface SerializedCustomization { sentryDSN?: string; locale: Locale; messages: Messages; + visualizationColors: VisualizationColors; } export interface ClientCustomization { @@ -155,6 +159,7 @@ export interface ClientCustomization { sentryDSN?: string; locale: Locale; messages: Messages; + visualizationColors: VisualizationColors; } function verifyCssVariables(cssVariables: Record): CssVariables { @@ -171,6 +176,12 @@ function verifyCssVariables(cssVariables: Record): CssVariables }, {}); } +function readVisualizationColors(config: CustomizationJS): VisualizationColors { + if (isNil(config.visualizationColors)) return DEFAULT_COLORS; + + return { ...DEFAULT_COLORS, ...config.visualizationColors }; +} + export function fromConfig(config: CustomizationJS = {}): Customization { const { title = DEFAULT_TITLE, @@ -193,6 +204,8 @@ export function fromConfig(config: CustomizationJS = {}): Customization { ? configExternalViews.map(ExternalView.fromJS) : []; + const visualizationColors = readVisualizationColors(config); + return { title, headerBackground, @@ -203,12 +216,13 @@ export function fromConfig(config: CustomizationJS = {}): Customization { timezones, locale: localeFromConfig(locale), messages, - externalViews + externalViews, + visualizationColors }; } export function serialize(customization: Customization): SerializedCustomization { - const { customLogoSvg, timezones, headerBackground, locale, externalViews, sentryDSN, urlShortener, messages } = customization; + const { customLogoSvg, timezones, headerBackground, locale, externalViews, sentryDSN, urlShortener, messages, visualizationColors } = customization; return { customLogoSvg, externalViews, @@ -217,7 +231,8 @@ export function serialize(customization: Customization): SerializedCustomization sentryDSN, locale: serializeLocale(locale), timezones: timezones.map(t => t.toJS()), - messages + messages, + visualizationColors }; } From 0078e496d793e78c560da916092801122fb373d2 Mon Sep 17 00:00:00 2001 From: Adrian Mroz Date: Wed, 23 Nov 2022 14:11:01 +0100 Subject: [PATCH 04/19] use colors from customization settings instead of hardcoded series --- .../improved-bar-chart/bar-chart.tsx | 4 ++- .../utils/bar-chart-model.ts | 14 ++++++----- .../charts/charts-per-series/series-chart.tsx | 5 ++-- .../series-hover-content.tsx | 10 +++++--- .../charts/charts-per-split/split-chart.tsx | 25 +++++++++++++------ .../charts-per-split/split-hover-content.tsx | 5 ++-- .../line-chart/legend/legend.tsx | 5 ++-- src/common/models/colors/colors.ts | 15 +---------- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/bar-chart.tsx b/src/client/visualizations/bar-chart/improved-bar-chart/bar-chart.tsx index 63efad50f..4255437ee 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/bar-chart.tsx +++ b/src/client/visualizations/bar-chart/improved-bar-chart/bar-chart.tsx @@ -25,6 +25,7 @@ import { MessageCard } from "../../../components/message-card/message-card"; import { Scroller } from "../../../components/scroller/scroller"; import { SPLIT } from "../../../config/constants"; import { selectMainDatum } from "../../../utils/dataset/selectors/selectors"; +import { useSettingsContext } from "../../../views/cube-view/settings-context"; import { Highlight } from "../../highlight-controller/highlight"; import { BarCharts } from "./bar-charts/bar-charts"; import { InteractionController } from "./interactions/interaction-controller"; @@ -49,8 +50,9 @@ interface BarChartProps { } export const BarChart: React.FunctionComponent = props => { + const { customization } = useSettingsContext(); const { dataset, essence, stage, highlight, acceptHighlight, dropHighlight, saveHighlight } = props; - const model = create(essence, dataset); + const model = create(essence, dataset, customization); const transposedDataset = transposeDataset(dataset, model); if (transposedDataset.length === 0) { diff --git a/src/client/visualizations/bar-chart/improved-bar-chart/utils/bar-chart-model.ts b/src/client/visualizations/bar-chart/improved-bar-chart/utils/bar-chart-model.ts index ba1c141cd..d17ccff09 100644 --- a/src/client/visualizations/bar-chart/improved-bar-chart/utils/bar-chart-model.ts +++ b/src/client/visualizations/bar-chart/improved-bar-chart/utils/bar-chart-model.ts @@ -17,7 +17,8 @@ import { Timezone } from "chronoshift"; import { List, OrderedMap } from "immutable"; import { Dataset, Datum } from "plywood"; -import { NORMAL_COLORS } from "../../../../../common/models/colors/colors"; +import { VisualizationColors } from "../../../../../common/models/colors/colors"; +import { ClientCustomization } from "../../../../../common/models/customization/customization"; import { Dimension } from "../../../../../common/models/dimension/dimension"; import { Essence } from "../../../../../common/models/essence/essence"; import { ConcreteSeries } from "../../../../../common/models/series/concrete-series"; @@ -70,17 +71,18 @@ function readCommons(essence: Essence): Omit { }; } -function createColorMap(nominalSplit: Split, dataset: Dataset): ColorMap { +function createColorMap(nominalSplit: Split, dataset: Dataset, colors: VisualizationColors): ColorMap { const datums = selectFirstSplitDatums(dataset); return datums.reduce((map: ColorMap, datum: Datum, i: number) => { const key = String(nominalSplit.selectValue(datum)); - const colorIndex = i % NORMAL_COLORS.length; - const color = NORMAL_COLORS[colorIndex]; + const colorIndex = i % colors.series.length; + const color = colors.series[colorIndex]; return map.set(key, color); }, OrderedMap()); } -export function create(essence: Essence, dataset: Dataset): BarChartModel { +export function create(essence: Essence, dataset: Dataset, customization: ClientCustomization): BarChartModel { + const { visualizationColors } = customization; const commons = readCommons(essence); if (!hasNominalSplit(essence)) { return { @@ -90,7 +92,7 @@ export function create(essence: Essence, dataset: Dataset): BarChartModel { } const nominalSplit = getNominalSplit(essence); const nominalDimension = getNominalDimension(essence); - const colors = createColorMap(nominalSplit, dataset); + const colors = createColorMap(nominalSplit, dataset, visualizationColors); return { ...commons, variant: ModelVariantId.STACKED, diff --git a/src/client/visualizations/line-chart/charts/charts-per-series/series-chart.tsx b/src/client/visualizations/line-chart/charts/charts-per-series/series-chart.tsx index 3de5912b3..cc74b88cb 100644 --- a/src/client/visualizations/line-chart/charts/charts-per-series/series-chart.tsx +++ b/src/client/visualizations/line-chart/charts/charts-per-series/series-chart.tsx @@ -16,7 +16,6 @@ import { Dataset, Datum, NumberRange, TimeRange } from "plywood"; import React from "react"; -import { NORMAL_COLORS } from "../../../../../common/models/colors/colors"; import { Essence } from "../../../../../common/models/essence/essence"; import { ConcreteSeries } from "../../../../../common/models/series/concrete-series"; import { Stage } from "../../../../../common/models/stage/stage"; @@ -32,6 +31,7 @@ import { extentAcrossSplits } from "../../utils/extent"; import { ContinuousTicks } from "../../utils/pick-x-axis-ticks"; import { getContinuousSplit, getNominalSplit, hasNominalSplit } from "../../utils/splits"; import { SeriesHoverContent } from "./series-hover-content"; +import { useSettingsContext } from "../../../../views/cube-view/settings-context"; interface SeriesChartProps { chartId: string; @@ -46,6 +46,7 @@ interface SeriesChartProps { } export const SeriesChart: React.FunctionComponent = props => { + const { customization: { visualizationColors } } = useSettingsContext(); const { chartId, interactions, visualisationStage, chartStage, essence, series, xScale, xTicks, dataset } = props; const hasComparison = essence.hasComparison(); const continuousSplitDataset = selectFirstSplitDataset(dataset); @@ -84,7 +85,7 @@ export const SeriesChart: React.FunctionComponent = props => { {({ yScale, lineStage }) => {continuousSplitDataset.data.map((datum, index) => { const splitKey = nominalSplit.selectValue(datum); - const color = NORMAL_COLORS[index]; + const color = visualizationColors.series[index]; return ; } -function colorEntries(dataset: Dataset, range: PlywoodRange, series: ConcreteSeries, essence: Essence): ColorEntry[] { +function colorEntries(dataset: Dataset, range: PlywoodRange, series: ConcreteSeries, essence: Essence, visualizationColors: VisualizationColors): ColorEntry[] { const { data } = dataset; const nominalSplit = getNominalSplit(essence); const continuousRef = getContinuousReference(essence); const hasComparison = essence.hasComparison(); return data.map((datum, i) => { const name = String(nominalSplit.selectValue(datum)); - const color = NORMAL_COLORS[i]; + const color = visualizationColors.series[i]; const hoverDatum = findSplitDatumByAttribute(datum, continuousRef, range); if (!hoverDatum) { @@ -75,9 +76,10 @@ interface SeriesHoverContentProps { } export const SeriesHoverContent: React.FunctionComponent = props => { + const { customization: { visualizationColors } } = useSettingsContext(); const { essence, range, series, dataset } = props; if (hasNominalSplit(essence)) { - const entries = colorEntries(dataset, range, series, essence); + const entries = colorEntries(dataset, range, series, essence, visualizationColors); return ; } return diff --git a/src/client/visualizations/line-chart/charts/charts-per-split/split-chart.tsx b/src/client/visualizations/line-chart/charts/charts-per-split/split-chart.tsx index 8e9751b01..dc7c4c561 100644 --- a/src/client/visualizations/line-chart/charts/charts-per-split/split-chart.tsx +++ b/src/client/visualizations/line-chart/charts/charts-per-split/split-chart.tsx @@ -16,12 +16,12 @@ import { Dataset, Datum, NumberRange, TimeRange } from "plywood"; import React from "react"; -import { NORMAL_COLORS } from "../../../../../common/models/colors/colors"; import { Essence } from "../../../../../common/models/essence/essence"; import { defaultFormatter } from "../../../../../common/models/series/series-format"; import { Stage } from "../../../../../common/models/stage/stage"; import { Unary } from "../../../../../common/utils/functional/functional"; import { selectSplitDataset } from "../../../../utils/dataset/selectors/selectors"; +import { useSettingsContext } from "../../../../views/cube-view/settings-context"; import { BaseChart } from "../../base-chart/base-chart"; import { ColoredSeriesChartLine } from "../../chart-line/colored-series-chart-line"; import { SingletonSeriesChartLine } from "../../chart-line/singleton-series-chart-line"; @@ -47,18 +47,29 @@ interface SplitChartProps { } export const SplitChart: React.FunctionComponent = props => { - const { chartId, interactions, visualisationStage, chartStage, essence, xScale, xTicks, selectDatum, dataset } = props; + const { customization: { visualizationColors } } = useSettingsContext(); + const { + chartId, + interactions, + visualisationStage, + chartStage, + essence, + xScale, + xTicks, + selectDatum, + dataset + } = props; const { interaction } = interactions; const splitDatum = selectDatum(dataset); const splitDataset = selectSplitDataset(splitDatum); const series = essence.getConcreteSeries(); - const label =