From 73b6ee7d890cfea97ec68de957e0ab1b58698f47 Mon Sep 17 00:00:00 2001 From: Anton Kulyk <kuliks.anton@gmail.com> Date: Thu, 5 Jan 2023 18:22:49 +0000 Subject: [PATCH] Merge MetabaseSettings and metabase-types settings type (#27528) * Add missing fields to `Settings` type * Make `MetabaseSettings` use up-to-date setting types * Fix type errors --- .../components/FontWidget/FontWidget.tsx | 2 +- .../src/metabase-types/api/mocks/settings.ts | 43 +++++- frontend/src/metabase-types/api/settings.ts | 45 ++++++- .../ModelCachingControl.tsx | 2 +- frontend/src/metabase/lib/pulse.ts | 5 +- frontend/src/metabase/lib/settings.ts | 124 ++++-------------- frontend/src/metabase/lib/time.ts | 8 +- frontend/src/metabase/setup/actions.ts | 2 +- .../containers/LanguageStep/LanguageStep.tsx | 2 +- 9 files changed, 118 insertions(+), 115 deletions(-) diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/FontWidget/FontWidget.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/FontWidget/FontWidget.tsx index 3de0e814ed2..e2946491f4f 100644 --- a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/FontWidget/FontWidget.tsx +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/FontWidget/FontWidget.tsx @@ -15,7 +15,7 @@ export interface FontWidgetProps { const FontWidget = ({ setting, settingValues, - availableFonts = MetabaseSettings.get("available-fonts"), + availableFonts = MetabaseSettings.get("available-fonts") || [], onChange, onChangeSetting, }: FontWidgetProps): JSX.Element => { diff --git a/frontend/src/metabase-types/api/mocks/settings.ts b/frontend/src/metabase-types/api/mocks/settings.ts index c6e135944fb..3133f5a1ed9 100644 --- a/frontend/src/metabase-types/api/mocks/settings.ts +++ b/frontend/src/metabase-types/api/mocks/settings.ts @@ -7,6 +7,8 @@ import { Settings, TokenFeatures, Version, + VersionInfo, + VersionInfoRecord, } from "metabase-types/api"; export const createMockEngine = (opts?: Partial<Engine>): Engine => ({ @@ -64,6 +66,24 @@ export const createMockVersion = (opts?: Partial<Version>): Version => ({ ...opts, }); +export const createMockVersionInfoRecord = ( + opts?: Partial<VersionInfoRecord>, +): VersionInfoRecord => ({ + version: "v1", + released: "2021-01-01", + patch: true, + highlights: ["Bug fix"], + ...opts, +}); + +export const createMockVersionInfo = ( + opts?: Partial<VersionInfo>, +): VersionInfo => ({ + latest: createMockVersionInfoRecord(), + older: [createMockVersionInfoRecord()], + ...opts, +}); + export const createMockTokenStatus = () => ({ status: "Token is Valid.", valid: true, @@ -108,19 +128,29 @@ export const createMockSettingDefinition = ( }); export const createMockSettings = (opts?: Partial<Settings>): Settings => ({ + "admin-email": "admin@metabase.test", + "anon-tracking-enabled": false, "application-font": "Lato", "application-font-files": [], + "application-name": "Metabase", "available-fonts": [], "available-locales": null, + "cloud-gateway-ips": null, "custom-formatting": {}, "deprecation-notice-version": undefined, "email-configured?": false, "enable-embedding": false, + "enable-enhancements?": false, "enable-nested-queries": true, "enable-query-caching": undefined, + "enable-password-login": true, "enable-public-sharing": false, "enable-xrays": false, "experimental-enable-actions": false, + engines: createMockEngines(), + "has-user-setup": true, + "hide-embed-branding?": true, + "ga-enabled": false, "google-auth-auto-create-accounts-domain": null, "google-auth-client-id": null, "google-auth-configured": false, @@ -131,11 +161,18 @@ export const createMockSettings = (opts?: Partial<Settings>): Settings => ({ "ldap-configured?": false, "ldap-enabled": false, "loading-message": "doing-science", + "other-sso-enabled?": null, + "password-complexity": { total: 6, digit: 1 }, "persisted-models-enabled": false, + "premium-embedding-token": null, "report-timezone-short": "UTC", "saml-configured": false, "saml-enabled": false, + "snowplow-url": "", + "search-typeahead-enabled": true, + "setup-token": null, "session-cookies": null, + "snowplow-enabled": false, "show-database-syncing-modal": false, "show-homepage-data": false, "show-homepage-pin-message": false, @@ -144,13 +181,17 @@ export const createMockSettings = (opts?: Partial<Settings>): Settings => ({ "show-metabot": true, "site-locale": "en", "site-url": "http://localhost:3000", + "site-uuid": "1234", "slack-app-token": null, "slack-files-channel": null, "slack-token": null, "slack-token-valid?": false, + "subscription-allowed-domains": null, "token-features": createMockTokenFeatures(), "token-status": null, - engines: createMockEngines(), + "user-locale": null, version: createMockVersion(), + "version-info": createMockVersionInfo(), + "version-info-last-checked": null, ...opts, }); diff --git a/frontend/src/metabase-types/api/settings.ts b/frontend/src/metabase-types/api/settings.ts index 489f6d80a75..9e51ab3c6e8 100644 --- a/frontend/src/metabase-types/api/settings.ts +++ b/frontend/src/metabase-types/api/settings.ts @@ -100,7 +100,19 @@ export interface FontFile { export type FontFormat = "woff" | "woff2" | "truetype"; export interface Version { - tag: string; + tag?: string; +} + +export interface VersionInfoRecord { + version?: string; // tag + released?: string; // year-month-day + patch?: boolean; + highlights?: string[]; +} + +export interface VersionInfo { + latest?: VersionInfoRecord; + older?: VersionInfoRecord[]; } export type LocaleData = [string, string]; @@ -128,43 +140,65 @@ export interface TokenFeatures { whitelabel: boolean; } +export type PasswordComplexity = { + total?: number; + digit?: number; +}; + export interface SettingDefinition { key: string; env_name?: string; is_env_setting: boolean; - value: unknown; + value?: unknown; } export interface Settings { + "admin-email": string; + "anon-tracking-enabled": boolean; "application-font": string; "application-font-files": FontFile[] | null; + "application-name": string; "available-fonts": string[]; "available-locales": LocaleData[] | null; + "cloud-gateway-ips": string[] | null; "custom-formatting": FormattingSettings; "deprecation-notice-version"?: string; "email-configured?": boolean; "embedding-secret-key"?: string; "enable-embedding": boolean; + "enable-enhancements?": boolean; "enable-nested-queries": boolean; "enable-query-caching"?: boolean; + "enable-password-login": boolean; "enable-public-sharing": boolean; "enable-xrays": boolean; + engines: Record<string, Engine>; "experimental-enable-actions": boolean; + "ga-enabled": boolean; "google-auth-auto-create-accounts-domain": string | null; "google-auth-client-id": string | null; "google-auth-configured": boolean; "google-auth-enabled": boolean; + "has-user-setup": boolean; + "hide-embed-branding?": boolean; "is-hosted?": boolean; "jwt-enabled"?: boolean; "jwt-configured"?: boolean; "ldap-configured?": boolean; "ldap-enabled": boolean; "loading-message": LoadingMessage; + "other-sso-enabled?": boolean | null; + "password-complexity": PasswordComplexity; "persisted-models-enabled": boolean; + "premium-embedding-token": string | null; "report-timezone-short": string; "saml-configured"?: boolean; "saml-enabled"?: boolean; + "search-typeahead-enabled": boolean; + "setup-token": string | null; "session-cookies": boolean | null; + "snowplow-enabled": boolean; + "snowplow-url": string; "show-database-syncing-modal": boolean; "show-homepage-data": boolean; "show-homepage-pin-message": boolean; @@ -172,15 +206,20 @@ export interface Settings { "show-lighthouse-illustration": boolean; "show-metabot": boolean; "site-locale": string; + "site-uuid": string; "site-url": string; "slack-app-token": string | null; "slack-files-channel": string | null; "slack-token": string | null; "slack-token-valid?": boolean; + "subscription-allowed-domains": string | null; "token-features": TokenFeatures; "token-status": TokenStatus | null; - engines: Record<string, Engine>; + "user-locale": string | null; version: Version; + "version-info": VersionInfo | null; + "version-info-last-checked": string | null; } export type SettingKey = keyof Settings; +0; diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/ModelCachingControl/ModelCachingControl.tsx b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/ModelCachingControl/ModelCachingControl.tsx index c6a04b1abef..a1ad7f44d99 100644 --- a/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/ModelCachingControl/ModelCachingControl.tsx +++ b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/ModelCachingControl/ModelCachingControl.tsx @@ -59,7 +59,7 @@ function ModelCachingControl({ database }: Props) { ? t`Turn model caching off` : t`Turn model caching on`; - const siteUUID = MetabaseSettings.get("site-uuid"); + const siteUUID = MetabaseSettings.get("site-uuid") || ""; const cacheSchemaName = getModelCacheSchemaName(databaseId, siteUUID); const handleCachingChange = async () => { diff --git a/frontend/src/metabase/lib/pulse.ts b/frontend/src/metabase/lib/pulse.ts index 7354dd6c9dd..1a8dc5a54ca 100644 --- a/frontend/src/metabase/lib/pulse.ts +++ b/frontend/src/metabase/lib/pulse.ts @@ -102,7 +102,10 @@ export function recipientIsValid(recipient: NotificationRecipient) { const recipientDomain = MetabaseUtils.getEmailDomain(recipient.email); const allowedDomains = MetabaseSettings.subscriptionAllowedDomains(); - return _.isEmpty(allowedDomains) || allowedDomains.includes(recipientDomain); + return ( + _.isEmpty(allowedDomains) || + (recipientDomain && allowedDomains.includes(recipientDomain)) + ); } export function pulseIsValid(pulse: Pulse, channelSpecs: ChannelSpecs) { diff --git a/frontend/src/metabase/lib/settings.ts b/frontend/src/metabase/lib/settings.ts index ae4df6eb8b7..01cfa20f10a 100644 --- a/frontend/src/metabase/lib/settings.ts +++ b/frontend/src/metabase/lib/settings.ts @@ -1,9 +1,12 @@ import _ from "underscore"; import { t, ngettext, msgid } from "ttag"; import moment from "moment-timezone"; + import { parseTimestamp } from "metabase/lib/time"; import MetabaseUtils from "metabase/lib/utils"; +import { PasswordComplexity, SettingKey, Settings } from "metabase-types/api"; + const n2w = (n: number) => MetabaseUtils.numberToWord(n); const PASSWORD_COMPLEXITY_CLAUSES = { @@ -50,85 +53,21 @@ const PASSWORD_COMPLEXITY_CLAUSES = { }, }; -// TODO: dump this from backend settings definitions -export type SettingName = - | "application-name" - | "admin-email" - | "analytics-uuid" - | "anon-tracking-enabled" - | "site-locale" - | "user-locale" - | "available-locales" - | "available-timezones" - | "custom-formatting" - | "custom-geojson" - | "email-configured?" - | "enable-embedding" - | "enable-enhancements?" - | "enable-public-sharing" - | "enable-xrays" - | "experimental-enable-actions" - | "persisted-models-enabled" - | "engines" - | "ga-code" - | "ga-enabled" - | "google-auth-enabled" - | "google-auth-client-id" - | "has-sample-database?" - | "has-user-setup" - | "hide-embed-branding?" - | "is-hosted?" - | "ldap-enabled" - | "ldap-configured?" - | "other-sso-enabled?" - | "enable-password-login" - | "map-tile-server-url" - | "password-complexity" - | "persisted-model-refresh-interval-hours" - | "premium-features" - | "search-typeahead-enabled" - | "setup-token" - | "site-url" - | "site-uuid" - | "token-status" - | "types" - | "version-info-last-checked" - | "version-info" - | "version" - | "subscription-allowed-domains" - | "cloud-gateway-ips" - | "snowplow-enabled" - | "snowplow-url" - | "deprecation-notice-version" - | "show-database-syncing-modal" - | "premium-embedding-token" - | "metabase-store-managed" - | "application-colors" - | "application-font" - | "available-fonts" - | "enable-query-caching" - | "start-of-week" - | "report-timezone-short"; - -type SettingsMap = Record<SettingName, any>; // provides access to Metabase application settings - type SettingListener = (value: any) => void; -class Settings { - _settings: Partial<SettingsMap>; - _listeners: Partial<Record<SettingName, SettingListener[]>> = {}; +class MetabaseSettings { + _settings: Partial<Settings>; + _listeners: Partial<{ [key: string]: SettingListener[] }> = {}; - constructor(settings: Partial<SettingsMap> = {}) { + constructor(settings: Partial<Settings> = {}) { this._settings = settings; } - get(key: SettingName, defaultValue: any = null) { - return this._settings[key] !== undefined - ? this._settings[key] - : defaultValue; + get<T extends SettingKey>(key: T): Partial<Settings>[T] { + return this._settings[key]; } - set(key: SettingName, value: any) { + set<T extends SettingKey>(key: T, value: Settings[T]) { if (this._settings[key] !== value) { this._settings[key] = value; const listeners = this._listeners[key]; @@ -143,15 +82,15 @@ class Settings { } } - setAll(settings: SettingsMap) { - const keys = Object.keys(settings) as SettingName[]; + setAll(settings: Settings) { + const keys = Object.keys(settings) as SettingKey[]; keys.forEach(key => { this.set(key, settings[key]); }); } - on(key: SettingName, callback: SettingListener) { + on(key: SettingKey, callback: SettingListener) { this._listeners[key] = this._listeners[key] || []; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._listeners[key]!.push(callback); @@ -166,12 +105,12 @@ class Settings { return this.get("enable-enhancements?"); } - isEmailConfigured() { - return this.get("email-configured?"); + isEmailConfigured(): boolean { + return !!this.get("email-configured?"); } isHosted(): boolean { - return this.get("is-hosted?"); + return !!this.get("is-hosted?"); } cloudGatewayIps(): string[] { @@ -267,7 +206,7 @@ class Settings { } docsUrl(page = "", anchor = "") { - let { tag } = this.get("version", {}); + let { tag } = this.get("version") || {}; const matches = tag && tag.match(/v[01]\.(\d+)(?:\.\d+)?(-.*)?/); if (matches) { @@ -330,27 +269,13 @@ class Settings { return result != null && result >= 0; } - /* - We expect the versionInfo to take on the JSON structure detailed below. - The 'older' section should contain only the last 5 previous versions, we don't need to go on forever. - The highlights for a version should just be text and should be limited to 5 items tops. - type VersionInfo = { - latest: Version, - older: Version[] - }; - type Version = { - version: string, // e.x. "v0.17.1" - released: ISO8601Time, - patch: bool, - highlights: string[] - }; - */ versionInfo() { - return this.get("version-info", {}); + return this.get("version-info") || {}; } currentVersion() { - return this.get("version", {}).tag; + const version = this.get("version") || {}; + return version.tag; } latestVersion() { @@ -366,9 +291,8 @@ class Settings { return this.isHosted() || this.isEnterprise(); } - // returns a map that looks like {total: 6, digit: 1} - passwordComplexityRequirements() { - return this.get("password-complexity", {}); + passwordComplexityRequirements(): PasswordComplexity { + return this.get("password-complexity") || {}; } /** @@ -399,7 +323,7 @@ class Settings { } } - subscriptionAllowedDomains() { + subscriptionAllowedDomains(): string[] { const setting = this.get("subscription-allowed-domains") || ""; return setting ? setting.split(",") : []; } @@ -414,4 +338,4 @@ function makeRegexTest(property: string, regex: RegExp) { const initValues = typeof window !== "undefined" ? _.clone(window.MetabaseBootstrap) : null; -export default new Settings(initValues); +export default new MetabaseSettings(initValues); diff --git a/frontend/src/metabase/lib/time.ts b/frontend/src/metabase/lib/time.ts index ad9d4f9a618..ee04d4d39ea 100644 --- a/frontend/src/metabase/lib/time.ts +++ b/frontend/src/metabase/lib/time.ts @@ -1,9 +1,5 @@ import { t } from "ttag"; -import moment, { - DurationInputArg1, - DurationInputArg2, - MomentInput, -} from "moment-timezone"; +import moment, { DurationInputArg2, MomentInput } from "moment-timezone"; import MetabaseSettings from "metabase/lib/settings"; @@ -110,7 +106,7 @@ export function getDefaultTimezone() { export function getNumericDateStyleFromSettings() { const dateStyle = getDateStyleFromSettings(); - return /\//.test(dateStyle) ? dateStyle : "M/D/YYYY"; + return dateStyle && /\//.test(dateStyle) ? dateStyle : "M/D/YYYY"; } export function getRelativeTime(timestamp: string) { diff --git a/frontend/src/metabase/setup/actions.ts b/frontend/src/metabase/setup/actions.ts index b323447d1fa..4b18b567d92 100644 --- a/frontend/src/metabase/setup/actions.ts +++ b/frontend/src/metabase/setup/actions.ts @@ -52,7 +52,7 @@ export const LOAD_LOCALE_DEFAULTS = "metabase/setup/LOAD_LOCALE_DEFAULTS"; export const loadLocaleDefaults = createThunkAction( LOAD_LOCALE_DEFAULTS, () => async (dispatch: any) => { - const data = MetabaseSettings.get("available-locales"); + const data = MetabaseSettings.get("available-locales") || []; const locale = getDefaultLocale(getLocales(data)); await dispatch(setLocale(locale)); }, diff --git a/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx b/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx index 8641aae459a..cafd4ce45c9 100644 --- a/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx +++ b/frontend/src/metabase/setup/containers/LanguageStep/LanguageStep.tsx @@ -14,7 +14,7 @@ import { const mapStateToProps = (state: State) => ({ locale: getLocale(state), - localeData: Settings.get("available-locales"), + localeData: Settings.get("available-locales") || [], isStepActive: isStepActive(state, LANGUAGE_STEP), isStepCompleted: isStepCompleted(state, LANGUAGE_STEP), isSetupCompleted: isSetupCompleted(state), -- GitLab