Compare commits

...

16 Commits

Author SHA1 Message Date
Timofei
0b83180e96 add more debug #1 2025-11-26 14:08:35 +04:00
Timofei
ffaca12661 add more debug 2025-11-25 19:11:20 +04:00
Timofei
6acd24fc20 trigger sample app e2e 2025-11-25 17:09:49 +04:00
Timofei
2352d01b34 Adjust debug 2025-11-25 16:26:26 +04:00
Timofei
90b238f722 Adjust debug 2025-11-25 14:46:27 +04:00
Timofei
12e1e7a8ae Debug 2025-11-24 21:05:51 +04:00
Denis Berezin
0b42ad89a9 Fix trend viz dates diff calculation for a year unit (#65985) 2025-11-24 18:34:33 +03:00
dpsutton
857bdadd45 bump databricks (#66123)
* bump databricks

This moves over to a thin jar (or regular jar) which provides jdbc
classes and lists its dependencies. We can now bump or pin as we
like. The previous versions were uberjars that had their dependencies
packaged inside.

```clojure
clj -Sdeps '{:deps {com.databricks/databricks-jdbc-thin {:mvn/version "3.0.5"}}}'
Clojure 1.12.1
user=> (Class/forName "com.databricks.client.jdbc.Driver")
com.databricks.client.jdbc.Driver
```

```clojure
clj -Xdeps tree :extra '{:deps {com.databricks/databricks-jdbc-thin {:mvn/version "3.0.5"}}}'
org.clojure/clojure 1.12.1
  . org.clojure/spec.alpha 0.5.238
  . org.clojure/core.specs.alpha 0.4.74
com.databricks/databricks-jdbc-thin 3.0.5
  . com.databricks/databricks-sdk-java 0.69.0
    X org.slf4j/slf4j-api 2.0.9 :older-version
    . org.apache.commons/commons-configuration2 2.11.0 :newer-version
      X org.apache.commons/commons-lang3 3.14.0 :older-version
      . org.apache.commons/commons-text 1.12.0
        X org.apache.commons/commons-lang3 3.14.0 :older-version
      . commons-logging/commons-logging 1.3.2 :newer-version
    X com.fasterxml.jackson.core/jackson-databind 2.15.2 :older-version
...
```

contrasted with

```clojure
❯ clj -Xdeps tree :extra '{:deps {com.databricks/databricks-jdbc {:mvn/version "3.0.1"}}}'
org.clojure/clojure 1.12.1
  . org.clojure/spec.alpha 0.5.238
  . org.clojure/core.specs.alpha 0.4.74
com.databricks/databricks-jdbc 3.0.1
```

* bump to run drivers

* include test namespace change to trigger driver tests
2025-11-24 09:14:19 -06:00
Nemanja Glumac
eed6736230 Fix uberjar fetching for SDK host-apps (#66194) 2025-11-24 15:53:22 +01:00
Noah Moss
144b0b33f8 Move audit installation after search init; change some logs to debug (#66045)
* Move audit installation after search init; change some logs to debug

* change another warn to debug
2025-11-24 14:48:35 +00:00
Sébastien
dcca252247 Closes EMB-997 add a reset function to plugins (#65958)
* reorg

* added reinit fns to oss plugins

* ಠ_ಠ

* Added doc and eslint to prevent import of individual reinitialize functions

* added short doc to reinitialize functions to indicate they're internal
2025-11-24 15:15:15 +01:00
Denis Berezin
a183e8b5b4 Add translation for boolean dash filter value (#66050)
* Add translation for boolean dash filter value

* Fix CI
2025-11-24 16:55:44 +03:00
Edward Paget
3b837a15a0 fix: google auth returns given_name/family_name (#66151)
this was incorrectly using first_name/last_name. This also updates the
existing mocks to return the correct fields.
2025-11-24 13:01:08 +00:00
Nemanja Glumac
6679039b45 Revert "Pin the NodeJS version to the version 22 (#66152)" (#66187)
This reverts commit 58867cac0b.
2025-11-24 12:37:57 +01:00
Nemanja Glumac
58867cac0b Pin the NodeJS version to the version 22 (#66152) 2025-11-24 11:49:49 +01:00
Nemanja Glumac
2e4b7adbcb Parallelize embedding SDK package and uberjar builds (#66122) 2025-11-24 11:47:29 +01:00
56 changed files with 2117 additions and 1184 deletions

View File

@@ -180,6 +180,7 @@ module.exports = {
"no-unconditional-metabase-links-render": "error",
"no-color-literals": "error",
"no-literal-metabase-strings": "error",
"no-oss-reinitialize-import": "error",
"depend/ban-dependencies": [
"error",
{

View File

@@ -16,11 +16,6 @@ frontend_ci: &frontend_ci
- ".github/actions/prepare-backend/**"
- ".github/workflows/frontend.yml"
frontend_embedding_sdk_ci: &frontend_embedding_sdk_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/embedding-sdk*.yml"
frontend_configs: &frontend_configs
- "yarn.lock"
- "**/tsconfig*.json"
@@ -127,14 +122,19 @@ e2e_ci: &e2e_ci
e2e_specs: &e2e_specs
- e2e/!(test-host-app)**/*.cy.*.{js,ts}
e2e_embedding_sdk:
- *frontend_embedding_sdk_ci
embedding_sdk_ci: &embedding_sdk_ci
- ".github/actions/prepare-frontend/**"
- ".github/actions/prepare-backend/**"
- ".github/workflows/embedding-sdk*.yml"
embedding_sdk_components:
- *embedding_sdk_ci
- *sources
- "e2e/component/**/*.cy.*.{js,ts}"
e2e_embedding_sdk_compatibility:
embedding_sdk_host_sample_apps:
- *default
- *frontend_embedding_sdk_ci
- *embedding_sdk_ci
- *frontend_sources
- *backend_sources
- "e2e/embedding-sdk-host-apps/**"

View File

@@ -2,10 +2,6 @@ name: Embedding SDK Component Tests
on:
workflow_call:
inputs:
cached-embedding-sdk-dist-artifact-name:
required: true
type: string
jobs:
sdk-component-tests:
@@ -44,7 +40,7 @@ jobs:
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare front-end environment
@@ -192,7 +188,7 @@ jobs:
- name: Retrieve Embedding SDK dist artifact built from the current commit N
uses: actions/download-artifact@v4
with:
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ github.workspace }}/resources/embedding-sdk
- name: Prepare Cypress environment

View File

@@ -2,10 +2,6 @@ name: "[NOT REQUIRED] SDK Compatibility tests"
on:
workflow_call:
inputs:
cached-embedding-sdk-dist-artifact-name:
required: true
type: string
jobs:
get-sample-apps-data:
@@ -197,7 +193,8 @@ jobs:
continue-on-error: true
env:
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
run: |
node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
- name: Upload Cypress Artifacts upon failure
uses: actions/upload-artifact@v4
@@ -253,9 +250,11 @@ jobs:
- uses: actions/checkout@v4
- name: Retrieve uberjar artifact for ee
uses: actions/download-artifact@v4
uses: ./.github/actions/fetch-artifact
with:
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
extract-path: "target/uberjar"
output-name: "metabase.jar"
- name: Retrieve Embedding SDK dist artifact
uses: actions/download-artifact@v4

View File

@@ -0,0 +1,31 @@
name: Embedding SDK Package Build
on:
workflow_call:
workflow_dispatch:
concurrency:
group: embedding-sdk-package-${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
build-sdk-package:
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build Embedding SDK package
run: |
# Remove the matchers to not report false positives on every PR
# unrelated to embedding (EMB-765)
echo "::remove-matcher owner=tsc::"
yarn build-embedding-sdk-package
- name: Prepare Embedding SDK artifact
uses: actions/upload-artifact@v4
with:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: resources/embedding-sdk

View File

@@ -2,62 +2,35 @@ name: SDK
on:
workflow_call:
inputs:
cli-snippets-type-check:
description: "Whether to run cli snippets type check."
required: true
type: boolean
docs-snippets-type-check:
description: "Whether to run docs snippets type check."
required: true
type: boolean
documentation-validation:
description: "Whether to run documentation generation validation check."
required: true
type: boolean
component-tests:
description: "Whether to run component tests."
required: true
type: boolean
host-sample-apps-tests:
description: "Whether to run host and sample apps compatibility tests."
required: true
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
name: Check which files changed
runs-on: ubuntu-22.04
timeout-minutes: 3
outputs:
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
e2e_embedding_sdk_compatibility: ${{ steps.changes.outputs.e2e_embedding_sdk_compatibility }}
frontend_embedding_sdk_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_sources }}
embedding_documentation: ${{ steps.changes.outputs.embedding_documentation }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
build-sdk-package:
needs: [files-changed]
if: |
!cancelled() &&
(needs.files-changed.outputs.e2e_embedding_sdk == 'true' ||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' ||
needs.files-changed.outputs.embedding_documentation == 'true')
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build Embedding SDK package
run: |
# Remove the matchers to not report false positives on every PR
# unrelated to embedding (EMB-765)
echo "::remove-matcher owner=tsc::"
yarn build-embedding-sdk-package
- name: Prepare Embedding SDK artifact
uses: actions/upload-artifact@v4
with:
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
path: resources/embedding-sdk
embedding-sdk-cli-snippets-type-check:
needs: [files-changed, build-sdk-package]
if: |
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' &&
needs.build-sdk-package.result == 'success'
if: inputs.cli-snippets-type-check
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -73,10 +46,7 @@ jobs:
run: yarn run embedding-sdk:cli-snippets:type-check
embedding-sdk-docs-snippets-type-check:
needs: [files-changed, build-sdk-package]
if: |
needs.files-changed.outputs.embedding_documentation == 'true' &&
needs.build-sdk-package.result == 'success'
if: inputs.docs-snippets-type-check
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -92,10 +62,7 @@ jobs:
run: yarn run embedding-sdk:docs-snippets:type-check
embedding-sdk-api-documentation-generation-validation:
needs: [files-changed, build-sdk-package]
if: |
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' &&
needs.build-sdk-package.result == 'success'
if: inputs.documentation-validation
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
@@ -143,21 +110,15 @@ jobs:
# See `Component e2e tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
embedding-sdk-component-tests:
name: "SDK component tests"
needs: [build-sdk-package, files-changed]
if: needs.files-changed.outputs.e2e_embedding_sdk == 'true'
if: inputs.component-tests
uses: ./.github/workflows/embedding-sdk-component-tests.yml
secrets: inherit
with:
cached-embedding-sdk-dist-artifact-name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
not-required-embedding-sdk-apps-compatibility-tests:
name: "SDK e2e tests for compatibility with (host/sample) apps"
needs: [build-sdk-package, files-changed]
if: needs.files-changed.outputs.e2e_embedding_sdk_compatibility == 'true'
uses: ./.github/workflows/embedding-sdk-e2e-tests.yml
if: inputs.host-sample-apps-tests
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
secrets: inherit
with:
cached-embedding-sdk-dist-artifact-name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
embedding-sdk-tests-result:
needs:

View File

@@ -25,6 +25,11 @@ jobs:
frontend_all: ${{ steps.changes.outputs.frontend_all }}
frontend_sources: ${{ steps.changes.outputs.frontend_sources }}
documentation: ${{ steps.changes.outputs.documentation }}
frontend_embedding_sdk_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_sources }}
embedding_documentation: ${{ steps.changes.outputs.embedding_documentation }}
embedding_sdk_components: ${{ steps.changes.outputs.embedding_sdk_components }}
embedding_sdk_host_sample_apps: ${{ steps.changes.outputs.embedding_sdk_host_sample_apps }}
embedding_sdk_ci: ${{ steps.changes.outputs.embedding_sdk_ci }}
steps:
- uses: actions/checkout@v4
- name: Test which files changed
@@ -92,10 +97,30 @@ jobs:
# list of changed specs files, then only specs are changed and we can run only those specs
specs: ${{ needs.files-changed.outputs.e2e_all_files == needs.files-changed.outputs.e2e_specs_files && needs.files-changed.outputs.e2e_specs_files || '' }}
embedding-sdk-package:
needs: [files-changed]
if: |
!cancelled() &&
(needs.files-changed.outputs.embedding_sdk_components == 'true' ||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' ||
needs.files-changed.outputs.embedding_documentation == 'true' ||
needs.files-changed.outputs.embedding_sdk_ci == 'true')
uses: ./.github/workflows/embedding-sdk-package-build.yml
secrets: inherit
sdk-tests:
needs: [uberjar]
needs: [uberjar, embedding-sdk-package, files-changed]
if: |
needs.embedding-sdk-package.result == 'success' &&
needs.uberjar.result == 'success'
uses: ./.github/workflows/embedding-sdk.yml
secrets: inherit
with:
cli-snippets-type-check: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
docs-snippets-type-check: ${{ needs.files-changed.outputs.embedding_documentation == 'true' }}
documentation-validation: ${{ needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' }}
component-tests: ${{ needs.files-changed.outputs.embedding_sdk_components == 'true' }}
host-sample-apps-tests: ${{ needs.files-changed.outputs.embedding_sdk_host_sample_apps == 'true' }}
pr-env:
needs: [uberjar]

View File

@@ -2,12 +2,14 @@ import { shell } from "../../../cypress-runner-utils";
import { waitForHealth } from "../../shared/helpers/wait-for-health";
export async function startContainers({
appName,
cwd,
env,
dockerUpCommand,
dockerDownCommand,
healthcheckPorts,
}: {
appName: string;
cwd: string;
env: Record<string, string | number>;
dockerUpCommand: string;
@@ -28,7 +30,11 @@ export async function startContainers({
});
await Promise.all(healthcheckPromises);
} catch {
} catch (e) {
console.log(e);
shell(`docker logs ${appName}-metabase-1`);
shell(`docker logs ${appName}-client-1`);
shell(`docker logs ${appName}-server-1`);
shell(dockerDownCommand, { cwd, env });
}
}

View File

@@ -50,6 +50,7 @@ export async function startSampleAppContainers(
});
setupAppCleanup({
appName,
rootPath,
env,
appDownCommand: dockerDownCommand,
@@ -69,6 +70,7 @@ export async function startSampleAppContainers(
}
await startContainers({
appName,
cwd: rootPath,
env,
dockerUpCommand,

View File

@@ -3,11 +3,13 @@ import fs from "fs";
import { shell } from "../../../cypress-runner-utils";
export function setupAppCleanup({
appName,
rootPath,
env,
appDownCommand,
cleanupAppDir,
}: {
appName: string;
rootPath: string;
env: Record<string, string | number>;
appDownCommand: string;
@@ -18,6 +20,7 @@ export function setupAppCleanup({
process.on(signal, () => {
console.log(`Parent received ${signal}, stopping app...`);
shell(`docker logs ${appName}-metabase-1`);
shell(appDownCommand, { cwd: rootPath, env });
if (cleanupAppDir) {

View File

@@ -676,7 +676,6 @@ describe("scenarios > content translation > static embedding > dashboards", () =
);
cy.wait("@cardQuery");
H.filterWidget().contains("vrai");
cy.findByTestId("table-body").within(() => {
cy.findAllByText(/vrai/).should("have.length", 2);
cy.findAllByText(/true/).should("have.length", 0);

View File

@@ -82,10 +82,10 @@ describe(
cy.log("assert click behavior");
H.getDashboardCard().findAllByText("true").first().click();
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
H.getDashboardCard().findByText("1 row").should("be.visible");
H.getDashboardCard().findAllByText("true").first().click();
H.filterWidget().findByText("true").should("not.exist");
H.filterWidget().findByText("True").should("not.exist");
H.getDashboardCard().findByText("200 rows").should("be.visible");
});
@@ -128,7 +128,7 @@ describe(
cy.log("assert click behavior");
H.getDashboardCard().findAllByText("true").first().click();
H.dashboardHeader().findByText(DASHBOARD_NAME).should("be.visible");
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
});
it("should allow to use a 'Go to a custom destination - Dashboard' click behavior with a parameter", () => {
@@ -141,7 +141,7 @@ describe(
H.popover().button("Add filter").click();
H.getDashboardCard().findAllByText("true").first().click();
H.dashboardHeader().findByText(DASHBOARD_NAME).should("be.visible");
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
});
});
@@ -166,7 +166,7 @@ describe(
H.getDashboardCard().findByText(QUESTION_NAME).click();
H.queryBuilderHeader().findByText(QUESTION_NAME).should("be.visible");
H.assertQueryBuilderRowCount(1);
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
});
it("should allow to use a 'Go to a custom destination - Saved question' click behavior", () => {
@@ -195,7 +195,7 @@ describe(
cy.log("assert click behavior");
H.getDashboardCard().findAllByText("true").first().click();
H.assertTableRowsCount(1);
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
});
it("should allow to use a 'Go to a custom destination - URL' click behavior", () => {
@@ -227,7 +227,7 @@ describe(
cy.log("assert click behavior");
H.getDashboardCard().findAllByText("true").first().click();
H.assertTableRowsCount(1);
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
},
);
});
@@ -253,7 +253,7 @@ describe(
H.getDashboardCard().findByText(QUESTION_NAME).click();
H.queryBuilderHeader().findByText(QUESTION_NAME).should("be.visible");
H.assertQueryBuilderRowCount(53);
H.filterWidget().findByText("true").should("be.visible");
H.filterWidget().findByText("True").should("be.visible");
});
it("should allow to use boolean parameters mapped to SQL query parameters in a public dashboard", () => {

View File

@@ -89,7 +89,7 @@
(let [pgvector (semantic.env/get-pgvector-datasource!)
index-metadata (semantic.env/get-index-metadata)]
(if-not (index-active? pgvector index-metadata)
(log/warn "update-index! called prior to init!")
(log/debug "update-index! called prior to init!")
(semantic.pgvector-api/gate-updates!
pgvector
index-metadata
@@ -102,7 +102,7 @@
(let [pgvector (semantic.env/get-pgvector-datasource!)
index-metadata (semantic.env/get-index-metadata)]
(if-not (index-active? pgvector index-metadata)
(log/warn "delete-from-index! called prior to init!")
(log/debug "delete-from-index! called prior to init!")
(semantic.pgvector-api/gate-deletes!
pgvector
index-metadata
@@ -132,7 +132,7 @@
(let [pgvector (semantic.env/get-pgvector-datasource!)
index-metadata (semantic.env/get-index-metadata)]
(if-not (index-active? pgvector index-metadata)
(log/warn "repair-index! called prior to init!")
(log/debug "repair-index! called prior to init!")
(semantic.repair/with-repair-table!
pgvector
(fn [repair-table-name]

View File

@@ -118,7 +118,7 @@
(group-by :id)
(map (fn [[id updates]]
(when (< 1 (count updates))
(log/warnf "More than one (%d) update issued for a gate entry: %s" (count updates) id))
(log/debugf "More than one (%d) update issued for a gate entry: %s" (count updates) id))
(peek updates)))
seq)]
(let [{:keys [gate-table-name]} index-metadata

View File

@@ -0,0 +1,61 @@
/**
* @fileoverview Rule to prevent importing reinitialize functions from OSS plugin files
*/
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
const ERROR_MESSAGE =
'Direct import of "reinitialize" from OSS plugin files is not allowed. Reinitializing all plugins at once from "metabase/plugins/index.ts" is the way.';
// eslint-disable-next-line import/no-commonjs
module.exports = {
meta: {
type: "problem",
docs: {
description:
"Disallow importing reinitialize functions from OSS plugin files",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
noReinitializeImport: ERROR_MESSAGE,
},
},
create(context) {
const filename = context.getFilename();
// Allow imports in the main plugins index file
if (filename.endsWith("metabase/plugins/index.ts")) {
return {};
}
return {
ImportDeclaration(node) {
const sourceValue = node.source.value;
// Check if importing from an OSS plugin file
if (
typeof sourceValue === "string" &&
sourceValue.startsWith("metabase/plugins/oss/")
) {
// Check if importing 'reinitialize'
const reinitializeImport = node.specifiers.find(
(specifier) =>
specifier.type === "ImportSpecifier" &&
specifier.imported.name === "reinitialize",
);
if (reinitializeImport) {
context.report({
node: reinitializeImport,
message: ERROR_MESSAGE,
});
}
}
},
};
},
};

View File

@@ -1,3 +1,5 @@
import { t } from "ttag";
import { Ellipsified } from "metabase/common/components/Ellipsified";
import { ParameterFieldWidgetValue } from "metabase/parameters/components/widgets/ParameterFieldWidget/ParameterFieldWidgetValue/ParameterFieldWidgetValue";
import { formatParameterValue } from "metabase/parameters/utils/formatting";
@@ -8,6 +10,7 @@ import {
isFieldFilterUiParameter,
} from "metabase-lib/v1/parameters/utils/parameter-fields";
import {
isBooleanParameter,
isDateParameter,
isStringParameter,
isTemporalUnitParameter,
@@ -22,7 +25,7 @@ import type {
export type FormattedParameterValueProps = {
parameter: UiParameter;
value: string | number | number[];
value: string | number | number[] | ParameterValue;
cardId?: CardId;
dashboardId?: DashboardId;
placeholder?: string;
@@ -47,7 +50,9 @@ function FormattedParameterValue({
(value) => getValue(value)?.toString() === first?.toString(),
);
const label = getLabel(displayValue);
const label = !isBooleanParameter(parameter)
? getLabel(displayValue)
: getBooleanLabel(first as boolean);
const renderContent = () => {
if (
@@ -101,7 +106,7 @@ function getValue(
}
function getLabel(
value: string | ParameterValue | undefined,
value: boolean | string | ParameterValue | undefined,
): string | undefined {
if (Array.isArray(value)) {
return value[1];
@@ -109,5 +114,9 @@ function getLabel(
return value?.toString();
}
function getBooleanLabel(value: boolean) {
return value ? t`True` : t`False`;
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default FormattedParameterValue;

View File

@@ -1,5 +1,11 @@
import { renderWithProviders, screen } from "__support__/ui";
import { setLocalization } from "metabase/lib/i18n";
import { createMockParameter } from "metabase-types/api/mocks";
import type { State } from "metabase-types/store";
import {
createMockSettingsState,
createMockState,
} from "metabase-types/store/mocks";
import FormattedParameterValue, {
type FormattedParameterValueProps,
@@ -7,13 +13,23 @@ import FormattedParameterValue, {
type SetupOpts = FormattedParameterValueProps;
function setup({ parameter, value, placeholder }: SetupOpts) {
function setup({
parameter,
value,
placeholder,
storeInitialState,
}: SetupOpts & {
storeInitialState?: Partial<State>;
}) {
return renderWithProviders(
<FormattedParameterValue
parameter={parameter}
value={value}
placeholder={placeholder}
/>,
{
storeInitialState,
},
);
}
@@ -45,4 +61,37 @@ describe("FormattedParameterValue", () => {
expect(screen.getByText("B")).toBeInTheDocument();
});
it("should translate boolean filter value", () => {
setLocalization({
headers: {
language: "fr",
"plural-forms": "nplurals=2; plural=(n != 1);",
},
translations: {
"": {
True: {
msgstr: ["Vrai"],
},
False: {
msgstr: ["Faux"],
},
},
},
});
setup({
value: [false],
parameter: createMockParameter({
name: "Boolean",
slug: "boolean",
type: "boolean/=",
}),
storeInitialState: createMockState({
settings: createMockSettingsState({ "site-locale": "fr" }),
}),
});
expect(screen.getByText("Faux")).toBeInTheDocument();
});
});

View File

@@ -14,6 +14,7 @@ import {
isFieldFilterParameter,
isTemporalUnitParameter,
} from "metabase-lib/v1/parameters/utils/parameter-type";
import type { ParameterValue, RowValue } from "metabase-types/api";
import { formatDateValue } from "./date-formatting";
@@ -38,14 +39,14 @@ function formatWithInferredType(value: any, parameter: UiParameter) {
}
export function formatParameterValue(
value: string | number | number[],
rawValue: string | number | number[] | ParameterValue,
parameter: UiParameter,
) {
if (Array.isArray(value) && value.length > 1) {
return renderNumberOfSelections(value.length);
if (Array.isArray(rawValue) && rawValue.length > 1) {
return renderNumberOfSelections(rawValue.length);
}
value = Array.isArray(value) ? value[0] : value;
const value: RowValue = Array.isArray(rawValue) ? rawValue[0] : rawValue;
if (isDateParameter(parameter)) {
return formatDateValue(parameter, String(value));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
import type { ComponentType } from "react";
import React from "react";
import type { MetabotContext } from "metabase/metabot";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type Question from "metabase-lib/v1/Question";
import type {
DashCardId,
SearchModel,
Timeline,
TimelineEvent,
VisualizationDisplay,
} from "metabase-types/api";
import type { AdminPath, State } from "metabase-types/store";
export type PluginAiSqlFixer = {
FixSqlQueryButton: ComponentType<Record<string, never>>;
};
export interface AIDashboardAnalysisSidebarProps {
onClose?: () => void;
dashcardId?: DashCardId;
}
export interface AIQuestionAnalysisSidebarProps {
question: Question;
className?: string;
onClose?: () => void;
timelines?: Timeline[];
visibleTimelineEvents?: TimelineEvent[];
}
export type PluginAIEntityAnalysis = {
AIQuestionAnalysisButton: ComponentType<any>;
AIQuestionAnalysisSidebar: ComponentType<AIQuestionAnalysisSidebarProps>;
AIDashboardAnalysisSidebar: ComponentType<AIDashboardAnalysisSidebarProps>;
canAnalyzeQuestion: (question: Question) => boolean;
chartAnalysisRenderFormats: {
[display in VisualizationDisplay]?: "png" | "svg" | "none";
};
};
type PluginMetabotConfig = {
emptyText?: string;
hideSuggestedPrompts?: boolean;
preventClose?: boolean;
preventRetryMessage?: boolean;
suggestionModels: (SearchModel | "transform" | "user")[];
};
type PluginMetabotType = {
isEnabled: () => boolean;
Metabot: (props: {
hide?: boolean;
config?: PluginMetabotConfig;
}) => React.ReactElement | null;
defaultMetabotContextValue: MetabotContext;
MetabotContext: React.Context<MetabotContext>;
getMetabotProvider: () => ComponentType<{ children: React.ReactNode }>;
getAdminPaths: () => AdminPath[];
getAdminRoutes: () => React.ReactElement;
getMetabotRoutes: () => React.ReactElement | null;
MetabotAdminPage: ComponentType;
getMetabotVisible: (state: State) => boolean;
MetabotToggleButton: ComponentType<{ className?: string }>;
MetabotAppBarButton: ComponentType;
MetabotAdminAppBarButton: ComponentType;
};
const getDefaultMetabotContextValue = (): MetabotContext => ({
prompt: "",
setPrompt: () => {},
promptInputRef: undefined,
getChatContext: () => ({}) as any,
registerChatContextProvider: () => () => {},
});
const defaultMetabotContextValue: MetabotContext =
getDefaultMetabotContextValue();
const getDefaultPluginAiSqlFixer = (): PluginAiSqlFixer => ({
FixSqlQueryButton: PluginPlaceholder,
});
export const PLUGIN_AI_SQL_FIXER: PluginAiSqlFixer =
getDefaultPluginAiSqlFixer();
const getDefaultPluginAIEntityAnalysis = (): PluginAIEntityAnalysis => ({
AIQuestionAnalysisButton: PluginPlaceholder,
AIQuestionAnalysisSidebar: PluginPlaceholder,
AIDashboardAnalysisSidebar: PluginPlaceholder,
canAnalyzeQuestion: () => false,
chartAnalysisRenderFormats: {},
});
export const PLUGIN_AI_ENTITY_ANALYSIS: PluginAIEntityAnalysis =
getDefaultPluginAIEntityAnalysis();
const getDefaultPluginMetabot = (): PluginMetabotType => ({
isEnabled: () => false,
Metabot: (_props: { hide?: boolean; config?: PluginMetabotConfig }) =>
null as React.ReactElement | null,
defaultMetabotContextValue,
MetabotContext: React.createContext(defaultMetabotContextValue),
getMetabotProvider: () => {
return ({ children }) =>
React.createElement(
PLUGIN_METABOT.MetabotContext.Provider,
{ value: PLUGIN_METABOT.defaultMetabotContextValue },
children,
);
},
getAdminPaths: () => [],
getAdminRoutes: () => PluginPlaceholder as unknown as React.ReactElement,
getMetabotRoutes: () => null,
MetabotAdminPage: () => `placeholder`,
getMetabotVisible: () => false,
MetabotToggleButton: PluginPlaceholder,
MetabotAppBarButton: PluginPlaceholder,
MetabotAdminAppBarButton: PluginPlaceholder,
});
export const PLUGIN_METABOT: PluginMetabotType = getDefaultPluginMetabot();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_AI_SQL_FIXER, getDefaultPluginAiSqlFixer());
Object.assign(PLUGIN_AI_ENTITY_ANALYSIS, getDefaultPluginAIEntityAnalysis());
Object.assign(PLUGIN_METABOT, getDefaultPluginMetabot());
}

View File

@@ -0,0 +1,23 @@
import type { DashboardId, ParameterId } from "metabase-types/api";
const getDefaultPluginApi = () => ({
getRemappedCardParameterValueUrl: (
dashboardId: DashboardId,
parameterId: ParameterId,
) =>
`/api/card/${dashboardId}/params/${encodeURIComponent(parameterId)}/remapping`,
getRemappedDashboardParameterValueUrl: (
dashboardId: DashboardId,
parameterId: ParameterId,
) =>
`/api/dashboard/${dashboardId}/params/${encodeURIComponent(parameterId)}/remapping`,
});
export const PLUGIN_API = getDefaultPluginApi();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_API, getDefaultPluginApi());
}

View File

@@ -0,0 +1,32 @@
import type { ComponentType } from "react";
import type { LinkProps } from "metabase/common/components/Link";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type Question from "metabase-lib/v1/Question";
import type { Dashboard, Database as DatabaseType } from "metabase-types/api";
export type InsightsLinkProps = (
| {
question: Pick<Question, "id" | "collection">;
dashboard?: never;
}
| {
question?: never;
dashboard: Pick<Dashboard, "id" | "collection">;
}
) &
Omit<LinkProps, "to">;
const getDefaultPluginAudit = () => ({
isAuditDb: (_db: DatabaseType) => false,
InsightsLink: PluginPlaceholder as ComponentType<InsightsLinkProps>,
});
export const PLUGIN_AUDIT = getDefaultPluginAudit();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_AUDIT, getDefaultPluginAudit());
}

View File

@@ -0,0 +1,50 @@
import {
NotFoundPlaceholder,
PluginPlaceholder,
} from "metabase/plugins/components/PluginPlaceholder";
import type { User } from "metabase-types/api";
import type { GetAuthProviders } from "../types";
const getDefaultPluginAuthProviders = () => ({
isEnabled: () => false,
AuthSettingsPage: PluginPlaceholder,
UserProvisioningSettings: NotFoundPlaceholder,
SettingsSAMLForm: NotFoundPlaceholder,
SettingsJWTForm: NotFoundPlaceholder,
providers: [] as GetAuthProviders[],
});
export const PLUGIN_AUTH_PROVIDERS = getDefaultPluginAuthProviders();
const getDefaultPluginLdapFormFields = () => ({
LdapUserProvisioning: PluginPlaceholder,
LdapGroupMembershipFilter: PluginPlaceholder,
});
export const PLUGIN_LDAP_FORM_FIELDS = getDefaultPluginLdapFormFields();
const getDefaultPluginIsPasswordUser = (): ((user: User) => boolean)[] => [];
export const PLUGIN_IS_PASSWORD_USER = getDefaultPluginIsPasswordUser();
const getDefaultPluginAdminUserFormFields = () => ({
FormLoginAttributes: PluginPlaceholder,
});
export const PLUGIN_ADMIN_USER_FORM_FIELDS =
getDefaultPluginAdminUserFormFields();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_AUTH_PROVIDERS, getDefaultPluginAuthProviders());
Object.assign(PLUGIN_LDAP_FORM_FIELDS, getDefaultPluginLdapFormFields());
PLUGIN_IS_PASSWORD_USER.length = 0;
PLUGIN_IS_PASSWORD_USER.push(...getDefaultPluginIsPasswordUser());
Object.assign(
PLUGIN_ADMIN_USER_FORM_FIELDS,
getDefaultPluginAdminUserFormFields(),
);
}

View File

@@ -0,0 +1,64 @@
import type { ComponentType, Dispatch, SetStateAction } from "react";
import {
getPerformanceTabMetadata,
strategies,
} from "metabase/admin/performance/constants/complex";
import type { ModelWithClearableCache } from "metabase/admin/performance/types";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type { StackProps } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import type { CacheableDashboard, CacheableModel } from "metabase-types/api";
// Types
export type InvalidateNowButtonProps = {
targetId: number;
targetModel: ModelWithClearableCache;
targetName: string;
};
export type SidebarCacheSectionProps = {
item: CacheableDashboard | Question;
model: CacheableModel;
setPage: Dispatch<SetStateAction<"default" | "caching">>;
};
export type SidebarCacheFormProps = {
item: CacheableDashboard | Question;
model: CacheableModel;
onClose: () => void;
} & StackProps;
export type PreemptiveCachingSwitchProps = {
handleSwitchToggle: () => void;
};
const getDefaultPluginCaching = () => ({
isGranularCachingEnabled: () => false,
StrategyFormLauncherPanel: PluginPlaceholder as any,
GranularControlsExplanation: PluginPlaceholder as any,
SidebarCacheSection:
PluginPlaceholder as ComponentType<SidebarCacheSectionProps>,
SidebarCacheForm: PluginPlaceholder as ComponentType<
SidebarCacheFormProps & { onBack: () => void }
>,
InvalidateNowButton:
PluginPlaceholder as ComponentType<InvalidateNowButtonProps>,
hasQuestionCacheSection: (_question: Question) => false,
canOverrideRootStrategy: false,
strategies: strategies,
DashboardAndQuestionCachingTab: PluginPlaceholder as any,
StrategyEditorForQuestionsAndDashboards: PluginPlaceholder as any,
getTabMetadata: getPerformanceTabMetadata,
PreemptiveCachingSwitch:
PluginPlaceholder as ComponentType<PreemptiveCachingSwitchProps>,
});
export const PLUGIN_CACHING = getDefaultPluginCaching();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_CACHING, getDefaultPluginCaching());
}

View File

@@ -0,0 +1,119 @@
import type React from "react";
import type { ComponentType } from "react";
import { t } from "ttag";
import { UNABLE_TO_CHANGE_ADMIN_PERMISSIONS } from "metabase/admin/permissions/constants/messages";
import { getIconBase } from "metabase/lib/icon";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type { IconProps } from "metabase/ui";
import type {
BaseEntityId,
Bookmark,
Collection,
CollectionAuthorityLevelConfig,
CollectionEssentials,
CollectionId,
CollectionInstanceAnaltyicsConfig,
} from "metabase-types/api";
// Types
export type ItemWithCollection = { collection: CollectionEssentials };
type GetCollectionIdType = (
sourceCollectionId?: CollectionId | null,
) => CollectionId | null;
export type CollectionAuthorityLevelDisplayProps = {
collection: Collection;
};
export type CollectionAuthorityLevelIcon = ComponentType<
Omit<IconProps, "name" | "tooltip"> & {
collection: Pick<Collection, "authority_level">;
tooltip?: "default" | "belonging";
archived?: boolean;
}
>;
type CollectionInstanceAnalyticsIcon = React.ComponentType<
Omit<IconProps, "name"> & {
collection: Collection;
entity: "collection" | "question" | "model" | "dashboard" | "metric";
}
>;
type FormCollectionAuthorityLevelPicker = React.ComponentType<
React.HTMLAttributes<HTMLDivElement> & { name: string; title?: string }
>;
const AUTHORITY_LEVEL_REGULAR: CollectionAuthorityLevelConfig = {
type: null,
get name() {
return t`Regular`;
},
icon: "folder",
};
const getDefaultPluginCollections = () => ({
AUTHORITY_LEVEL: {
[JSON.stringify(AUTHORITY_LEVEL_REGULAR.type)]: AUTHORITY_LEVEL_REGULAR,
},
COLLECTION_TYPES: {
[JSON.stringify(AUTHORITY_LEVEL_REGULAR.type)]: AUTHORITY_LEVEL_REGULAR,
},
REGULAR_COLLECTION: AUTHORITY_LEVEL_REGULAR,
isRegularCollection: (_data: Partial<Collection> | Bookmark) => true,
isSyncedCollection: (_data: Partial<Collection>) => false,
getCollectionType: (
_collection: Partial<Collection>,
): CollectionAuthorityLevelConfig | CollectionInstanceAnaltyicsConfig =>
AUTHORITY_LEVEL_REGULAR,
useGetDefaultCollectionId: null as GetCollectionIdType | null,
CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID: "" as BaseEntityId | "",
INSTANCE_ANALYTICS_ADMIN_READONLY_MESSAGE: UNABLE_TO_CHANGE_ADMIN_PERMISSIONS,
getAuthorityLevelMenuItems: (
_collection: Collection,
_onUpdate: (collection: Collection, values: Partial<Collection>) => void,
): React.ReactNode[] => [],
getIcon: getIconBase,
filterOutItemsFromInstanceAnalytics: <Item extends ItemWithCollection>(
items: Item[],
) => items as Item[],
canCleanUp: (_collection: Collection) => false as boolean,
useGetCleanUpMenuItems: (
_collection: Collection,
): { menuItems: JSX.Element[] } => ({
menuItems: [],
}),
cleanUpRoute: null as React.ReactElement | null,
cleanUpAlert: (() => null) as (props: {
collection: Collection;
}) => JSX.Element | null,
});
export const PLUGIN_COLLECTIONS = getDefaultPluginCollections();
const getDefaultPluginCollectionComponents = () => ({
CollectionAuthorityLevelIcon:
PluginPlaceholder as CollectionAuthorityLevelIcon,
FormCollectionAuthorityLevelPicker:
PluginPlaceholder as FormCollectionAuthorityLevelPicker,
CollectionInstanceAnalyticsIcon:
PluginPlaceholder as CollectionInstanceAnalyticsIcon,
CollectionAuthorityLevelDisplay:
PluginPlaceholder as ComponentType<CollectionAuthorityLevelDisplayProps>,
});
export const PLUGIN_COLLECTION_COMPONENTS =
getDefaultPluginCollectionComponents();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_COLLECTIONS, getDefaultPluginCollections());
Object.assign(
PLUGIN_COLLECTION_COMPONENTS,
getDefaultPluginCollectionComponents(),
);
}

View File

@@ -0,0 +1,37 @@
import { useCallback } from "react";
import type { ContentTranslationFunction } from "metabase/i18n/types";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type { HoveredObject } from "metabase/visualizations/types";
import type { Series } from "metabase-types/api";
const getDefaultPluginContentTranslation = () => ({
isEnabled: false,
setEndpointsForStaticEmbedding: (_encodedToken: string) => {},
ContentTranslationConfiguration: PluginPlaceholder,
useTranslateContent: <
T = string | null | undefined,
>(): ContentTranslationFunction => {
return useCallback(<U = T>(arg: U) => arg, []);
},
translateDisplayNames: <T extends object>(
obj: T,
_tc: ContentTranslationFunction,
) => obj,
useTranslateFieldValuesInHoveredObject: (obj?: HoveredObject | null) => obj,
useTranslateSeries: (obj: Series) => obj,
useSortByContentTranslation: () => (a: string, b: string) =>
a.localeCompare(b),
});
export const PLUGIN_CONTENT_TRANSLATION = getDefaultPluginContentTranslation();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(
PLUGIN_CONTENT_TRANSLATION,
getDefaultPluginContentTranslation(),
);
}

View File

@@ -0,0 +1,45 @@
import type { SearchFilterComponent } from "metabase/search/types";
import type { CollectionEssentials } from "metabase-types/api";
import type { State } from "metabase-types/store";
export type ModelFilterControlsProps = any;
export type ModelFilterSettings = {
verified: boolean;
};
export type MetricFilterControlsProps = any;
export type MetricFilterSettings = {
verified: boolean;
};
const getDefaultPluginContentVerification = () => ({
contentVerificationEnabled: false,
VerifiedFilter: {} as SearchFilterComponent<"verified">,
sortCollectionsByVerification: (
_a: CollectionEssentials,
_b: CollectionEssentials,
) => 0,
ModelFilterControls: (_props: ModelFilterControlsProps) => null,
getDefaultModelFilters: (_state: State): ModelFilterSettings => ({
verified: false,
}),
getDefaultMetricFilters: (_state: State): MetricFilterSettings => ({
verified: false,
}),
MetricFilterControls: (_props: MetricFilterControlsProps) => null,
});
export const PLUGIN_CONTENT_VERIFICATION =
getDefaultPluginContentVerification();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(
PLUGIN_CONTENT_VERIFICATION,
getDefaultPluginContentVerification(),
);
}

View File

@@ -0,0 +1,198 @@
import type { Middleware } from "@reduxjs/toolkit";
import type { ComponentType } from "react";
import { t } from "ttag";
import noResultsSource from "assets/img/no_results.svg";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type { UiParameter } from "metabase-lib/v1/parameters/types";
import type { Dashboard, Pulse } from "metabase-types/api";
import type { AdminPathKey, State } from "metabase-types/store";
// Types
export type IllustrationValue = {
src: string;
isDefault: boolean;
} | null;
interface PluginDashboardSubscriptionParametersSectionOverride {
Component?: ComponentType<{
className?: string;
parameters: UiParameter[];
hiddenParameters?: string;
dashboard: Dashboard;
pulse: Pulse;
setPulseParameters: (parameters: UiParameter[]) => void;
}>;
}
const defaultLandingPageIllustration = {
src: "app/img/bridge.svg",
isDefault: true,
};
const defaultLoginPageIllustration = {
src: "app/img/bridge.svg",
isDefault: true,
};
const getLoadingMessage = (isSlow: boolean | undefined = false) =>
isSlow ? t`Waiting for results...` : t`Doing science...`;
const getDefaultAppInitFunctions = (): (() => void)[] => [];
export const PLUGIN_APP_INIT_FUNCTIONS = getDefaultAppInitFunctions();
const getDefaultLandingPage = () => ({
getLandingPage: () => "/",
LandingPageWidget: PluginPlaceholder,
});
export const PLUGIN_LANDING_PAGE: {
getLandingPage: () => string | null | undefined;
LandingPageWidget: ComponentType;
} = getDefaultLandingPage();
const getDefaultReduxMiddlewares = (): Middleware[] => [];
export const PLUGIN_REDUX_MIDDLEWARES = getDefaultReduxMiddlewares();
const getDefaultLogoIconComponents = (): ComponentType[] => [];
export const PLUGIN_LOGO_ICON_COMPONENTS = getDefaultLogoIconComponents();
const getDefaultAdminAllowedPathGetters = (): ((
user: any,
) => AdminPathKey[])[] => [];
export const PLUGIN_ADMIN_ALLOWED_PATH_GETTERS =
getDefaultAdminAllowedPathGetters();
const getDefaultAdminTools = () => ({
COMPONENT: null,
});
export const PLUGIN_ADMIN_TOOLS: {
COMPONENT: ComponentType | null;
} = getDefaultAdminTools();
const getDefaultSelectors = () => ({
canWhitelabel: (_state: State) => false,
getLoadingMessageFactory: (_state: State) => getLoadingMessage,
getIsWhiteLabeling: (_state: State) => false,
// eslint-disable-next-line no-literal-metabase-strings -- This is the actual Metabase name, so we don't want to translate it.
getApplicationName: (_state: State) => "Metabase",
getShowMetabaseLinks: (_state: State) => true,
getLoginPageIllustration: (_state: State): IllustrationValue => {
return defaultLoginPageIllustration;
},
getLandingPageIllustration: (_state: State): IllustrationValue => {
return defaultLandingPageIllustration;
},
getNoDataIllustration: (_state: State): string | null => {
return noResultsSource;
},
getNoObjectIllustration: (_state: State): string | null => {
return noResultsSource;
},
});
export const PLUGIN_SELECTORS = getDefaultSelectors();
const getDefaultFormWidgets = (): Record<string, ComponentType<any>> => ({});
export const PLUGIN_FORM_WIDGETS = getDefaultFormWidgets();
const getDefaultSnippetSidebarPlusMenuOptions = () => [];
const getDefaultSnippetSidebarRowRenderers = () => ({});
const getDefaultSnippetSidebarModals = () => [];
const getDefaultSnippetSidebarHeaderButtons = () => [];
export const PLUGIN_SNIPPET_SIDEBAR_PLUS_MENU_OPTIONS =
getDefaultSnippetSidebarPlusMenuOptions();
export const PLUGIN_SNIPPET_SIDEBAR_ROW_RENDERERS =
getDefaultSnippetSidebarRowRenderers();
export const PLUGIN_SNIPPET_SIDEBAR_MODALS = getDefaultSnippetSidebarModals();
export const PLUGIN_SNIPPET_SIDEBAR_HEADER_BUTTONS =
getDefaultSnippetSidebarHeaderButtons();
const getDefaultDashboardSubscriptionParametersSectionOverride =
(): PluginDashboardSubscriptionParametersSectionOverride => ({
Component: undefined,
});
export const PLUGIN_DASHBOARD_SUBSCRIPTION_PARAMETERS_SECTION_OVERRIDE =
getDefaultDashboardSubscriptionParametersSectionOverride();
const getDefaultReducers = () => ({
applicationPermissionsPlugin: () => null,
sandboxingPlugin: () => null,
shared: () => null,
metabotPlugin: () => null,
documents: () => null,
remoteSyncPlugin: () => null,
});
export const PLUGIN_REDUCERS: {
applicationPermissionsPlugin: any;
sandboxingPlugin: any;
shared: any;
metabotPlugin: any;
documents: any;
remoteSyncPlugin: any;
} = getDefaultReducers();
const getDefaultIsEeBuild = () => ({
isEEBuild: () => false,
});
export const PLUGIN_IS_EE_BUILD = getDefaultIsEeBuild();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
PLUGIN_APP_INIT_FUNCTIONS.length = 0;
PLUGIN_APP_INIT_FUNCTIONS.push(...getDefaultAppInitFunctions());
Object.assign(PLUGIN_LANDING_PAGE, getDefaultLandingPage());
PLUGIN_REDUX_MIDDLEWARES.length = 0;
PLUGIN_REDUX_MIDDLEWARES.push(...getDefaultReduxMiddlewares());
PLUGIN_LOGO_ICON_COMPONENTS.length = 0;
PLUGIN_LOGO_ICON_COMPONENTS.push(...getDefaultLogoIconComponents());
PLUGIN_ADMIN_ALLOWED_PATH_GETTERS.length = 0;
PLUGIN_ADMIN_ALLOWED_PATH_GETTERS.push(
...getDefaultAdminAllowedPathGetters(),
);
Object.assign(PLUGIN_ADMIN_TOOLS, getDefaultAdminTools());
Object.assign(PLUGIN_SELECTORS, getDefaultSelectors());
Object.assign(PLUGIN_FORM_WIDGETS, getDefaultFormWidgets());
PLUGIN_SNIPPET_SIDEBAR_PLUS_MENU_OPTIONS.length = 0;
PLUGIN_SNIPPET_SIDEBAR_PLUS_MENU_OPTIONS.push(
...getDefaultSnippetSidebarPlusMenuOptions(),
);
Object.assign(
PLUGIN_SNIPPET_SIDEBAR_ROW_RENDERERS,
getDefaultSnippetSidebarRowRenderers(),
);
PLUGIN_SNIPPET_SIDEBAR_MODALS.length = 0;
PLUGIN_SNIPPET_SIDEBAR_MODALS.push(...getDefaultSnippetSidebarModals());
PLUGIN_SNIPPET_SIDEBAR_HEADER_BUTTONS.length = 0;
PLUGIN_SNIPPET_SIDEBAR_HEADER_BUTTONS.push(
...getDefaultSnippetSidebarHeaderButtons(),
);
Object.assign(
PLUGIN_DASHBOARD_SUBSCRIPTION_PARAMETERS_SECTION_OVERRIDE,
getDefaultDashboardSubscriptionParametersSectionOverride(),
);
Object.assign(PLUGIN_REDUCERS, getDefaultReducers());
Object.assign(PLUGIN_IS_EE_BUILD, getDefaultIsEeBuild());
}

View File

@@ -0,0 +1,27 @@
import type { DashCardMenuItem } from "metabase/dashboard/components/DashCard/DashCardMenu/dashcard-menu";
import type Question from "metabase-lib/v1/Question";
import type { DashCardId } from "metabase-types/api";
import type { Dispatch as ReduxDispatch } from "metabase-types/store";
type DashCardMenuItemGetter = (
question: Question,
dashcardId: DashCardId | undefined,
dispatch: ReduxDispatch,
) => (DashCardMenuItem & { key: string }) | null;
export type PluginDashcardMenu = {
dashcardMenuItemGetters: DashCardMenuItemGetter[];
};
const getDefaultPluginDashcardMenu = (): PluginDashcardMenu => ({
dashcardMenuItemGetters: [],
});
export const PLUGIN_DASHCARD_MENU = getDefaultPluginDashcardMenu();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_DASHCARD_MENU, getDefaultPluginDashcardMenu());
}

View File

@@ -0,0 +1,65 @@
import type { ComponentType } from "react";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type Database from "metabase-lib/v1/metadata/Database";
import type {
DatabaseData,
DatabaseId,
DatabaseLocalSettingAvailability,
Database as DatabaseType,
TableId,
} from "metabase-types/api";
const getDefaultPluginDbRouting = () => ({
DatabaseRoutingSection: PluginPlaceholder as ComponentType<{
database: DatabaseType;
}>,
getDatabaseNameFieldProps: (_isSlug: boolean) => ({}),
getDestinationDatabaseRoutes: (_IsAdmin: any) =>
null as React.ReactElement | null,
useRedirectDestinationDatabase: (
_database: Pick<DatabaseType, "id" | "router_database_id"> | undefined,
): void => {},
getPrimaryDBEngineFieldState: (
_database: Pick<Database, "router_user_attribute">,
): "default" | "hidden" | "disabled" => "default",
});
export const PLUGIN_DB_ROUTING = getDefaultPluginDbRouting();
const getDefaultPluginDatabaseReplication = () => ({
DatabaseReplicationSection: PluginPlaceholder as ComponentType<{
database: DatabaseType;
}>,
});
export const PLUGIN_DATABASE_REPLICATION =
getDefaultPluginDatabaseReplication();
const getDefaultPluginTableEditing = () => ({
isEnabled: () => false,
isDatabaseTableEditingEnabled: (_database: DatabaseType): boolean => false,
getRoutes: () => null as React.ReactElement | null,
getTableEditUrl: (_tableId: TableId, _databaseId: DatabaseId): string => "/",
AdminDatabaseTableEditingSection: PluginPlaceholder as ComponentType<{
database: DatabaseType;
settingsAvailable?: Record<string, DatabaseLocalSettingAvailability>;
updateDatabase: (
database: { id: DatabaseId } & Partial<DatabaseData>,
) => Promise<void>;
}>,
});
export const PLUGIN_TABLE_EDITING = getDefaultPluginTableEditing();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_DB_ROUTING, getDefaultPluginDbRouting());
Object.assign(
PLUGIN_DATABASE_REPLICATION,
getDefaultPluginDatabaseReplication(),
);
Object.assign(PLUGIN_TABLE_EDITING, getDefaultPluginTableEditing());
}

View File

@@ -0,0 +1,21 @@
import type React from "react";
import type { Document } from "metabase-types/api";
const getDefaultPluginDocuments = () => ({
getRoutes: () => null as React.ReactElement | null,
shouldShowDocumentInNewItemMenu: () => false,
getCurrentDocument: (_state: any) => null as Document | null,
getSidebarOpen: (_state: any) => false,
getCommentSidebarOpen: (_state: any) => false,
DocumentCopyForm: (_props: any) => null as React.ReactElement | null,
});
export const PLUGIN_DOCUMENTS = getDefaultPluginDocuments();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_DOCUMENTS, getDefaultPluginDocuments());
}

View File

@@ -0,0 +1,34 @@
import type { ReactNode } from "react";
export type SdkIframeEmbedSetupModalProps = {
opened: boolean;
onClose: () => void;
initialState?: SdkIframeEmbedSetupModalInitialState;
};
export type SdkIframeEmbedSetupModalInitialState = {
resourceType?: string | null;
resourceId?: string | number | null;
useExistingUserSession?: boolean;
};
const getDefaultPluginEmbeddingIframeSdkSetup = () => ({
isFeatureEnabled: () => false,
shouldShowEmbedInNewItemMenu: () => false,
SdkIframeEmbedSetupModal: (
_props: SdkIframeEmbedSetupModalProps,
): ReactNode => null,
});
export const PLUGIN_EMBEDDING_IFRAME_SDK_SETUP =
getDefaultPluginEmbeddingIframeSdkSetup();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(
PLUGIN_EMBEDDING_IFRAME_SDK_SETUP,
getDefaultPluginEmbeddingIframeSdkSetup(),
);
}

View File

@@ -0,0 +1,18 @@
import type { ReactNode } from "react";
const getDefaultPluginEmbeddingIframeSdk = () => ({
hasValidLicense: () => false,
SdkIframeEmbedRoute: (): ReactNode => null,
});
export const PLUGIN_EMBEDDING_IFRAME_SDK = getDefaultPluginEmbeddingIframeSdk();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(
PLUGIN_EMBEDDING_IFRAME_SDK,
getDefaultPluginEmbeddingIframeSdk(),
);
}

View File

@@ -0,0 +1,15 @@
const getDefaultPluginEmbeddingSdk = () => ({
isEnabled: () => false,
onBeforeRequestHandlers: {
getOrRefreshSessionHandler: async () => {},
},
});
export const PLUGIN_EMBEDDING_SDK = getDefaultPluginEmbeddingSdk();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_EMBEDDING_SDK, getDefaultPluginEmbeddingSdk());
}

View File

@@ -0,0 +1,31 @@
import type { ReactNode } from "react";
import type { DataSourceSelectorProps } from "metabase/embedding-sdk/types/components/data-picker";
import type { TableId } from "metabase-types/api";
import type { State } from "metabase-types/store";
import type { EmbeddingEntityType } from "metabase-types/store/embedding-data-picker";
export interface SimpleDataPickerProps {
filterByDatabaseId: number | null;
selectedEntity?: TableId;
isInitiallyOpen: boolean;
triggerElement: ReactNode;
setSourceTableFn: (tableId: TableId) => void;
entityTypes: EmbeddingEntityType[];
}
const getDefaultPluginEmbedding = () => ({
isEnabled: () => false,
isInteractiveEmbeddingEnabled: (_state: State) => false,
SimpleDataPicker: (_props: SimpleDataPickerProps): ReactNode => null,
DataSourceSelector: (_props: DataSourceSelectorProps): ReactNode => null,
});
export const PLUGIN_EMBEDDING = getDefaultPluginEmbedding();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_EMBEDDING, getDefaultPluginEmbedding());
}

View File

@@ -0,0 +1,12 @@
const getDefaultPluginEntities = () => ({
entities: {} as Record<string, any>,
});
export const PLUGIN_ENTITIES = getDefaultPluginEntities();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_ENTITIES, getDefaultPluginEntities());
}

View File

@@ -0,0 +1,23 @@
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type Question from "metabase-lib/v1/Question";
import type { ModelCacheRefreshStatus } from "metabase-types/api";
const getDefaultPluginModelPersistence = () => ({
isModelLevelPersistenceEnabled: () => false,
ModelCacheToggle: PluginPlaceholder as ({
persistedModel,
model,
}: {
persistedModel?: ModelCacheRefreshStatus;
model: Question;
}) => JSX.Element,
});
export const PLUGIN_MODEL_PERSISTENCE = getDefaultPluginModelPersistence();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_MODEL_PERSISTENCE, getDefaultPluginModelPersistence());
}

View File

@@ -0,0 +1,39 @@
import type { ColorName } from "metabase/lib/colors/types";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import type { IconName, IconProps } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import type { BaseUser, Dashboard } from "metabase-types/api";
export type RevisionOrModerationEvent = {
title: string;
timestamp: string;
icon: IconName | { name: IconName; color: ColorName } | Record<string, never>;
description?: string;
revision?: any;
};
const getDefaultPluginModeration = () => ({
isEnabled: () => false,
EntityModerationIcon: PluginPlaceholder,
QuestionModerationSection: PluginPlaceholder,
ModerationReviewBanner: PluginPlaceholder,
ModerationReviewTextForQuestion: PluginPlaceholder,
ModerationReviewTextForDashboard: PluginPlaceholder,
ModerationStatusIcon: PluginPlaceholder,
getQuestionIcon: PluginPlaceholder,
getStatusIcon: (_moderated_status?: string): string | IconProps | undefined =>
undefined,
getModerationTimelineEvents: (_reviews: any, _currentUser: BaseUser | null) =>
[] as RevisionOrModerationEvent[],
useDashboardMenuItems: (_model?: Dashboard, _reload?: () => void) => [],
useQuestionMenuItems: (_model?: Question, _reload?: () => void) => [],
});
export const PLUGIN_MODERATION = getDefaultPluginModeration();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_MODERATION, getDefaultPluginModeration());
}

View File

@@ -0,0 +1,264 @@
import type { ReactNode } from "react";
import {
type DataPermission,
DataPermissionValue,
type DatabaseEntityId,
type EntityId,
type PermissionSubject,
} from "metabase/admin/permissions/types";
import type Database from "metabase-lib/v1/metadata/Database";
import type {
Dataset,
Group,
GroupPermissions,
GroupsPermissions,
User,
} from "metabase-types/api";
import type { State } from "metabase-types/store";
import type { PluginGroupManagersType } from "../types";
// Types
export interface UserWithApplicationPermissions extends User {
permissions?: {
can_access_monitoring: boolean;
can_access_setting: boolean;
can_access_subscription: boolean;
};
}
const getDefaultAdminPermissionsDatabaseRoutes = () => [];
const getDefaultAdminPermissionsDatabaseGroupRoutes = () => [];
const getDefaultAdminPermissionsDatabasePostActions = () => ({
impersonated: null,
});
const getDefaultAdminPermissionsDatabaseActions = () => ({
impersonated: [],
});
const getDefaultAdminPermissionsTableOptions = () => [];
const getDefaultAdminPermissionsTableRoutes = () => [];
const getDefaultAdminPermissionsTableGroupRoutes = () => [];
const getDefaultAdminPermissionsTableFieldsOptions = () => [];
const getDefaultAdminPermissionsTableFieldsConfirmations = (): Array<
(
_permissions: GroupsPermissions,
_groupId: number,
_entityId: EntityId,
_value: DataPermissionValue,
) => any
> => [];
const getDefaultAdminPermissionsTableFieldsActions = () => ({
sandboxed: [],
});
const getDefaultAdminPermissionsTableFieldsPostAction = () => ({
sandboxed: null,
});
export const PLUGIN_ADMIN_PERMISSIONS_DATABASE_ROUTES =
getDefaultAdminPermissionsDatabaseRoutes();
export const PLUGIN_ADMIN_PERMISSIONS_DATABASE_GROUP_ROUTES =
getDefaultAdminPermissionsDatabaseGroupRoutes();
export const PLUGIN_ADMIN_PERMISSIONS_DATABASE_POST_ACTIONS =
getDefaultAdminPermissionsDatabasePostActions();
export const PLUGIN_ADMIN_PERMISSIONS_DATABASE_ACTIONS =
getDefaultAdminPermissionsDatabaseActions();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_OPTIONS =
getDefaultAdminPermissionsTableOptions();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_ROUTES =
getDefaultAdminPermissionsTableRoutes();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_GROUP_ROUTES =
getDefaultAdminPermissionsTableGroupRoutes();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_OPTIONS =
getDefaultAdminPermissionsTableFieldsOptions();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_CONFIRMATIONS =
getDefaultAdminPermissionsTableFieldsConfirmations();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_ACTIONS =
getDefaultAdminPermissionsTableFieldsActions();
export const PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_POST_ACTION =
getDefaultAdminPermissionsTableFieldsPostAction();
const getDefaultDataPermissions = () => ({
permissionsPayloadExtraSelectors: [],
hasChanges: [],
upgradeViewPermissionsIfNeeded: null,
shouldRestrictNativeQueryPermissions: () => false,
});
export const PLUGIN_DATA_PERMISSIONS: {
permissionsPayloadExtraSelectors: ((
state: State,
) => [Record<string, undefined | { group_id: string }[]>, string[]])[];
hasChanges: ((state: State) => boolean)[];
shouldRestrictNativeQueryPermissions: (
permissions: GroupsPermissions,
groupId: number,
entityId: EntityId,
permission: DataPermission,
value: DataPermissionValue,
database: Database,
) => boolean;
upgradeViewPermissionsIfNeeded:
| ((
permissions: GroupsPermissions,
groupId: number,
entityId: EntityId,
value: any,
database: Database,
permission: DataPermission,
) => GroupPermissions)
| null;
} = getDefaultDataPermissions();
const getDefaultAdminUserMenuItems = (): Array<
(user: User) => React.ReactNode
> => [];
const getDefaultAdminUserMenuRoutes = () => [];
export const PLUGIN_ADMIN_USER_MENU_ITEMS = getDefaultAdminUserMenuItems();
export const PLUGIN_ADMIN_USER_MENU_ROUTES = getDefaultAdminUserMenuRoutes();
const getDefaultAdvancedPermissions = () => ({
addDatabasePermissionOptions: (permissions: any[], _database: Database) =>
permissions,
addSchemaPermissionOptions: (permissions: any[], _value: string) =>
permissions,
addTablePermissionOptions: (permissions: any[], _value: string) =>
permissions,
getDatabaseLimitedAccessPermission: (_value: string) => null,
isAccessPermissionDisabled: (
_value: string,
_subject: "schemas" | "tables" | "fields",
) => false,
isRestrictivePermission: (_value: string) => false,
shouldShowViewDataColumn: false,
defaultViewDataPermission: DataPermissionValue.UNRESTRICTED,
});
export const PLUGIN_ADVANCED_PERMISSIONS = getDefaultAdvancedPermissions();
const getDefaultFeatureLevelPermissions = () => ({
getFeatureLevelDataPermissions: (
_entityId: DatabaseEntityId,
_groupId: number,
_isAdmin: boolean,
_permissions: GroupsPermissions,
_dataAccessPermissionValue: DataPermissionValue,
_defaultGroup: Group,
_permissionSubject: PermissionSubject,
) => {
return [] as any;
},
getDataColumns: (_subject: PermissionSubject) => [] as any,
getDownloadWidgetMessageOverride: (_result: Dataset): string | null => null,
canDownloadResults: (_result: Dataset): boolean => true,
dataModelQueryProps: {} as any,
databaseDetailsQueryProps: {} as any,
});
export const PLUGIN_FEATURE_LEVEL_PERMISSIONS =
getDefaultFeatureLevelPermissions();
const getDefaultApplicationPermissions = () => ({
getRoutes: (): ReactNode => null,
tabs: [] as any,
selectors: {
canAccessSettings: (_state: any) => false,
canManageSubscriptions: (_state: any) => true,
},
});
export const PLUGIN_APPLICATION_PERMISSIONS =
getDefaultApplicationPermissions();
const getDefaultGroupManagers = (): PluginGroupManagersType => ({
UserTypeToggle: () => null as any,
UserTypeCell: null,
getChangeMembershipConfirmation: () => null,
getRemoveMembershipConfirmation: () => null,
deleteGroup: null,
confirmDeleteMembershipAction: null,
confirmUpdateMembershipAction: null,
});
export const PLUGIN_GROUP_MANAGERS = getDefaultGroupManagers();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
PLUGIN_ADMIN_PERMISSIONS_DATABASE_ROUTES.length = 0;
PLUGIN_ADMIN_PERMISSIONS_DATABASE_ROUTES.push(
...getDefaultAdminPermissionsDatabaseRoutes(),
);
PLUGIN_ADMIN_PERMISSIONS_DATABASE_GROUP_ROUTES.length = 0;
PLUGIN_ADMIN_PERMISSIONS_DATABASE_GROUP_ROUTES.push(
...getDefaultAdminPermissionsDatabaseGroupRoutes(),
);
Object.assign(
PLUGIN_ADMIN_PERMISSIONS_DATABASE_POST_ACTIONS,
getDefaultAdminPermissionsDatabasePostActions(),
);
Object.assign(
PLUGIN_ADMIN_PERMISSIONS_DATABASE_ACTIONS,
getDefaultAdminPermissionsDatabaseActions(),
);
PLUGIN_ADMIN_PERMISSIONS_TABLE_OPTIONS.length = 0;
PLUGIN_ADMIN_PERMISSIONS_TABLE_OPTIONS.push(
...getDefaultAdminPermissionsTableOptions(),
);
PLUGIN_ADMIN_PERMISSIONS_TABLE_ROUTES.length = 0;
PLUGIN_ADMIN_PERMISSIONS_TABLE_ROUTES.push(
...getDefaultAdminPermissionsTableRoutes(),
);
PLUGIN_ADMIN_PERMISSIONS_TABLE_GROUP_ROUTES.length = 0;
PLUGIN_ADMIN_PERMISSIONS_TABLE_GROUP_ROUTES.push(
...getDefaultAdminPermissionsTableGroupRoutes(),
);
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_OPTIONS.length = 0;
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_OPTIONS.push(
...getDefaultAdminPermissionsTableFieldsOptions(),
);
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_CONFIRMATIONS.length = 0;
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_CONFIRMATIONS.push(
...getDefaultAdminPermissionsTableFieldsConfirmations(),
);
Object.assign(
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_ACTIONS,
getDefaultAdminPermissionsTableFieldsActions(),
);
Object.assign(
PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_POST_ACTION,
getDefaultAdminPermissionsTableFieldsPostAction(),
);
Object.assign(PLUGIN_DATA_PERMISSIONS, getDefaultDataPermissions());
PLUGIN_ADMIN_USER_MENU_ITEMS.length = 0;
PLUGIN_ADMIN_USER_MENU_ITEMS.push(...getDefaultAdminUserMenuItems());
PLUGIN_ADMIN_USER_MENU_ROUTES.length = 0;
PLUGIN_ADMIN_USER_MENU_ROUTES.push(...getDefaultAdminUserMenuRoutes());
Object.assign(PLUGIN_ADVANCED_PERMISSIONS, getDefaultAdvancedPermissions());
Object.assign(
PLUGIN_FEATURE_LEVEL_PERMISSIONS,
getDefaultFeatureLevelPermissions(),
);
Object.assign(
PLUGIN_APPLICATION_PERMISSIONS,
getDefaultApplicationPermissions(),
);
Object.assign(PLUGIN_GROUP_MANAGERS, getDefaultGroupManagers());
}

View File

@@ -0,0 +1,15 @@
import type React from "react";
const getDefaultPluginPublicSharing = () => ({
PublicDocumentRoute: (_props: any) => null as React.ReactElement | null,
PublicLinksDocumentListing: () => null as React.ReactElement | null,
});
export const PLUGIN_PUBLIC_SHARING = getDefaultPluginPublicSharing();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_PUBLIC_SHARING, getDefaultPluginPublicSharing());
}

View File

@@ -0,0 +1,44 @@
import type { TagDescription } from "@reduxjs/toolkit/query";
import type { ComponentType, ReactNode } from "react";
import {
NotFoundPlaceholder,
PluginPlaceholder,
} from "metabase/plugins/components/PluginPlaceholder";
import type { SyncedCollectionsSidebarSectionProps } from "../types";
const getDefaultPluginRemoteSync = () => ({
LibraryNav: PluginPlaceholder,
RemoteSyncSettings: NotFoundPlaceholder,
SyncedCollectionsSidebarSection: PluginPlaceholder,
REMOTE_SYNC_INVALIDATION_TAGS: null,
useSyncStatus: () => ({
isIdle: true,
taskType: null,
progress: 0,
message: "",
progressModal: null,
}),
});
export const PLUGIN_REMOTE_SYNC: {
LibraryNav: ComponentType;
RemoteSyncSettings: ComponentType;
SyncedCollectionsSidebarSection: ComponentType<SyncedCollectionsSidebarSectionProps>;
REMOTE_SYNC_INVALIDATION_TAGS: TagDescription<any>[] | null;
useSyncStatus: () => {
isIdle: boolean;
taskType: any;
progress: number;
message: string;
progressModal: ReactNode;
};
} = getDefaultPluginRemoteSync();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_REMOTE_SYNC, getDefaultPluginRemoteSync());
}

View File

@@ -0,0 +1,19 @@
import type { EmbedResourceDownloadOptions } from "metabase/public/lib/types";
const getDefaultPluginResourceDownloads = () => ({
areDownloadsEnabled: (_args: {
downloads?: string | boolean | null;
}): EmbedResourceDownloadOptions => ({
pdf: true,
results: true,
}),
});
export const PLUGIN_RESOURCE_DOWNLOADS = getDefaultPluginResourceDownloads();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_RESOURCE_DOWNLOADS, getDefaultPluginResourceDownloads());
}

View File

@@ -0,0 +1,14 @@
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
const getDefaultPluginSemanticSearch = () => ({
SearchSettingsWidget: PluginPlaceholder,
});
export const PLUGIN_SEMANTIC_SEARCH = getDefaultPluginSemanticSearch();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_SEMANTIC_SEARCH, getDefaultPluginSemanticSearch());
}

View File

@@ -0,0 +1,31 @@
import type { ComponentType } from "react";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
const getDefaultPluginAdminSettings = () => ({
InteractiveEmbeddingSettings: null,
LicenseAndBillingSettings: PluginPlaceholder,
useUpsellFlow: (_props: {
campaign: string;
location: string;
}): {
triggerUpsellFlow: (() => void) | undefined;
} => ({
triggerUpsellFlow: undefined,
}),
});
export const PLUGIN_ADMIN_SETTINGS: {
InteractiveEmbeddingSettings: ComponentType | null;
LicenseAndBillingSettings: ComponentType;
useUpsellFlow: (props: { campaign: string; location: string }) => {
triggerUpsellFlow: (() => void) | undefined;
};
} = getDefaultPluginAdminSettings();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_ADMIN_SETTINGS, getDefaultPluginAdminSettings());
}

View File

@@ -0,0 +1,20 @@
import type { ComponentType } from "react";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
const getDefaultPluginSmtpOverride = () => ({
CloudSMTPConnectionCard: PluginPlaceholder,
SMTPOverrideConnectionForm: PluginPlaceholder,
});
export const PLUGIN_SMTP_OVERRIDE: {
CloudSMTPConnectionCard: ComponentType;
SMTPOverrideConnectionForm: ComponentType<{ onClose: () => void }>;
} = getDefaultPluginSmtpOverride();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_SMTP_OVERRIDE, getDefaultPluginSmtpOverride());
}

View File

@@ -0,0 +1,22 @@
import type { ComponentType } from "react";
import { NotFoundPlaceholder } from "../components/PluginPlaceholder";
const getDefaultPluginSupport = () => ({
isEnabled: false,
SupportSettings: NotFoundPlaceholder,
GrantAccessModal: NotFoundPlaceholder,
});
export const PLUGIN_SUPPORT: {
isEnabled: boolean;
SupportSettings: ComponentType;
GrantAccessModal: ComponentType<{ onClose: VoidFunction }>;
} = getDefaultPluginSupport();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_SUPPORT, getDefaultPluginSupport());
}

View File

@@ -0,0 +1,165 @@
import type { ComponentType, Context, ReactNode } from "react";
import { createContext } from "react";
import {
NotFoundPlaceholder,
PluginPlaceholder,
} from "metabase/plugins/components/PluginPlaceholder";
import type Question from "metabase-lib/v1/Question";
import type {
CheckDependenciesResponse,
PythonTransformSourceDraft,
Transform,
TransformId,
UpdateSnippetRequest,
UpdateTransformRequest,
} from "metabase-types/api";
import type { AdminPath } from "metabase-types/store";
// Types
export type TransformPickerItem = {
id: TransformId;
name: string;
model: "transform";
};
export type TransformPickerProps = {
value: TransformPickerItem | undefined;
onItemSelect: (transform: TransformPickerItem) => void;
};
export type TransformsPlugin = {
TransformPicker: ComponentType<TransformPickerProps>;
getAdminPaths(): AdminPath[];
getAdminRoutes(): ReactNode;
};
export type PythonTransformEditorProps = {
name?: string;
source: PythonTransformSourceDraft;
proposedSource?: PythonTransformSourceDraft;
isNew: boolean;
isDirty: boolean;
isSaving: boolean;
onChangeSource: (source: PythonTransformSourceDraft) => void;
onSave: () => void;
onCancel: () => void;
onAcceptProposed: () => void;
onRejectProposed: () => void;
};
export type PythonTransformSourceSectionProps = {
transform: Transform;
};
export type PythonTransformsPlugin = {
isEnabled: boolean;
TransformEditor: ComponentType<PythonTransformEditorProps>;
SourceSection: ComponentType<PythonTransformSourceSectionProps>;
PythonRunnerSettingsPage: ComponentType;
getAdminRoutes: () => ReactNode;
getTransformsNavLinks: () => ReactNode;
};
type DependenciesPlugin = {
isEnabled: boolean;
DependencyGraphPage: ComponentType;
DependencyGraphPageContext: Context<DependencyGraphPageContextType>;
CheckDependenciesForm: ComponentType<CheckDependenciesFormProps>;
CheckDependenciesModal: ComponentType<CheckDependenciesModalProps>;
CheckDependenciesTitle: ComponentType;
useCheckCardDependencies: (
props: UseCheckDependenciesProps<Question>,
) => UseCheckDependenciesResult<Question>;
useCheckSnippetDependencies: (
props: UseCheckDependenciesProps<UpdateSnippetRequest>,
) => UseCheckDependenciesResult<UpdateSnippetRequest>;
useCheckTransformDependencies: (
props: UseCheckDependenciesProps<UpdateTransformRequest>,
) => UseCheckDependenciesResult<UpdateTransformRequest>;
};
export type DependencyGraphPageContextType = {
baseUrl?: string;
defaultEntry?: any;
};
export type CheckDependenciesFormProps = {
checkData: CheckDependenciesResponse;
onSave: () => void | Promise<void>;
onCancel: () => void;
};
export type CheckDependenciesModalProps = {
checkData: CheckDependenciesResponse;
opened: boolean;
onSave: () => void | Promise<void>;
onClose: () => void;
};
export type UseCheckDependenciesProps<TChange> = {
onSave: (change: TChange) => Promise<void>;
};
export type UseCheckDependenciesResult<TChange> = {
checkData?: CheckDependenciesResponse;
isCheckingDependencies: boolean;
isConfirmationShown: boolean;
handleInitialSave: (change: TChange) => Promise<void>;
handleSaveAfterConfirmation: () => Promise<void>;
handleCloseConfirmation: () => void;
};
function useCheckDependencies<TChange>({
onSave,
}: UseCheckDependenciesProps<TChange>): UseCheckDependenciesResult<TChange> {
return {
isConfirmationShown: false,
isCheckingDependencies: false,
handleInitialSave: onSave,
handleSaveAfterConfirmation: () => Promise.resolve(),
handleCloseConfirmation: () => undefined,
};
}
const getDefaultPluginTransforms = (): TransformsPlugin => ({
TransformPicker: PluginPlaceholder,
getAdminPaths: () => [],
getAdminRoutes: () => null,
});
export const PLUGIN_TRANSFORMS = getDefaultPluginTransforms();
const getDefaultPluginTransformsPython = (): PythonTransformsPlugin => ({
isEnabled: false,
TransformEditor: PluginPlaceholder,
SourceSection: PluginPlaceholder,
PythonRunnerSettingsPage: NotFoundPlaceholder,
getAdminRoutes: () => null,
getTransformsNavLinks: () => null,
});
export const PLUGIN_TRANSFORMS_PYTHON = getDefaultPluginTransformsPython();
const getDefaultPluginDependencies = (): DependenciesPlugin => ({
isEnabled: false,
DependencyGraphPage: PluginPlaceholder,
DependencyGraphPageContext: createContext({}),
CheckDependenciesForm: PluginPlaceholder,
CheckDependenciesModal: PluginPlaceholder,
CheckDependenciesTitle: PluginPlaceholder,
useCheckCardDependencies: useCheckDependencies,
useCheckSnippetDependencies: useCheckDependencies,
useCheckTransformDependencies: useCheckDependencies,
});
export const PLUGIN_DEPENDENCIES = getDefaultPluginDependencies();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_TRANSFORMS, getDefaultPluginTransforms());
Object.assign(PLUGIN_TRANSFORMS_PYTHON, getDefaultPluginTransformsPython());
Object.assign(PLUGIN_DEPENDENCIES, getDefaultPluginDependencies());
}

View File

@@ -0,0 +1,34 @@
import type { ComponentType } from "react";
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
import { _FileUploadErrorModal } from "metabase/status/components/FileUploadStatusLarge/FileUploadErrorModal";
type GdriveConnectionModalProps = {
isModalOpen: boolean;
onClose: () => void;
reconnect: boolean;
};
type GdriveAddDataPanelProps = {
onAddDataModalClose: () => void;
};
const getDefaultPluginUploadManagement = () => ({
FileUploadErrorModal: _FileUploadErrorModal,
UploadManagementTable: PluginPlaceholder,
GdriveSyncStatus: PluginPlaceholder,
GdriveConnectionModal:
PluginPlaceholder as ComponentType<GdriveConnectionModalProps>,
GdriveDbMenu: PluginPlaceholder,
GdriveAddDataPanel:
PluginPlaceholder as ComponentType<GdriveAddDataPanelProps>,
});
export const PLUGIN_UPLOAD_MANAGEMENT = getDefaultPluginUploadManagement();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_UPLOAD_MANAGEMENT, getDefaultPluginUploadManagement());
}

View File

@@ -0,0 +1,15 @@
import { PluginPlaceholder } from "metabase/plugins/components/PluginPlaceholder";
const getDefaultPluginWhitelabel = () => ({
WhiteLabelBrandingSettingsPage: PluginPlaceholder,
WhiteLabelConcealSettingsPage: PluginPlaceholder,
});
export const PLUGIN_WHITELABEL = getDefaultPluginWhitelabel();
/**
* @internal Do not call directly. Use the main reinitialize function from metabase/plugins instead.
*/
export function reinitialize() {
Object.assign(PLUGIN_WHITELABEL, getDefaultPluginWhitelabel());
}

View File

@@ -2,4 +2,4 @@
["src" "resources"]
:deps
{com.databricks/databricks-jdbc {:mvn/version "3.0.1"}}}
{com.databricks/databricks-jdbc-thin {:mvn/version "3.0.5"}}}

View File

@@ -258,7 +258,7 @@
(is (= "2017-04-18T09:53:37.046-07:00"
(last (first rows)))))))))))
(deftest additional-options-test
(deftest ^:synchronized additional-options-test
(mt/test-driver
:databricks
(testing "Connections with UserAgentEntry"

View File

@@ -217,8 +217,14 @@
[from to unit]
(when (and from to unit)
;; Make sure we work for both ascending and descending time series
(let [[from to] (sort [from to])]
(about= (- to from) (unit->duration unit)))))
(let [[from to] (sort [from to])
diff (- to from)]
(if (= unit :year)
;; Special handling for years: accept both 365 days (non-leap) and 366 days (leap year),
;; but reject anything less than 365 days
(and (>= diff 364.5)
(<= diff 366.5))
(about= diff (unit->duration unit))))))
(defn- infer-unit
[from to]

View File

@@ -207,15 +207,16 @@
;; otherwise update if appropriate
(sample-data/update-sample-database-if-needed!)))
(init-status/set-progress! 0.8))
(ensure-audit-db-installed!)
(notification/seed-notification!)
(init-status/set-progress! 0.9)
(init-status/set-progress! 0.85)
(embed.settings/check-and-sync-settings-on-startup! env/env)
(init-status/set-progress! 0.95)
(init-status/set-progress! 0.9)
(setting/migrate-encrypted-settings!)
(database/check-health!)
(startup/run-startup-logic!)
(init-status/set-progress! 0.95)
(ensure-audit-db-installed!)
(task/start-scheduler!)
(queue/start-listeners!)
(init-status/set-complete!)

View File

@@ -27,13 +27,14 @@
:else
(try
(let [token-info-response (http/post (format google-auth-token-info-url token))
{:keys [first_name last_name email]} (sso.google/google-auth-token-info token-info-response)]
(log/infof "Successfully authenticated Google Sign-In token for: %s %s" first_name last_name)
{first-name :given_name last-name :family_name :keys [email]}
(sso.google/google-auth-token-info token-info-response)]
(log/infof "Successfully authenticated Google Sign-In token for: %s %s" first-name last-name)
;; Always return user data - let login! handle account creation policy
{:success? true
:user-data {:email email
:first_name first_name
:last_name last_name
:first_name first-name
:last_name last-name
:sso_source :google}
:provider-id email})
(catch clojure.lang.ExceptionInfo e

View File

@@ -51,6 +51,15 @@
(is (true? (valid-period? #t "2015-01-01" #t "2015-01-08")))
(is (true? (valid-period? #t "2015-01-01" #t "2015-04-03")))
(is (true? (valid-period? #t "2015" #t "2016")))
;; Test leap year handling
(is (true? (valid-period? #t "2016" #t "2017")))
(is (true? (valid-period? #t "2017" #t "2018")))
;; Periods less than 365 days should NOT be inferred as :year
(is (not= :year (#'insights/infer-unit (inst->day #t "2015-01-01") (inst->day #t "2015-12-01"))))
(is (not= :year (#'insights/infer-unit (inst->day #t "2015-02-01") (inst->day #t "2016-01-01"))))
;; Both 365 and 366 day periods should be inferred as :year
(is (= :year (#'insights/infer-unit (inst->day #t "2015-01-01") (inst->day #t "2016-01-01"))))
(is (= :year (#'insights/infer-unit (inst->day #t "2016-01-01") (inst->day #t "2017-01-01"))))
(is (= false (valid-period? #t "2015-01-01" #t "2015-01-09")))
(is (true? (valid-period? #t "2015-01-01" #t "2015-04-03" :quarter)))
(is (= false (valid-period? #t "2015-01-01" #t "2015-04-03" :month)))

View File

@@ -546,8 +546,8 @@
{:status 200
:body (str "{\"aud\":\"pretend-client-id.apps.googleusercontent.com\","
"\"email_verified\":\"true\","
"\"first_name\":\"test\","
"\"last_name\":\"user\","
"\"given_name\":\"test\","
"\"family_name\":\"user\","
"\"email\":\"test@metabase.com\"}")})]
(testing "Test that 'remember me' checkbox sets expiration on session"
(let [response (mt/client-real-response :post 200 "session/google_auth" {:token "foo" :remember true})]
@@ -559,17 +559,23 @@
(testing "POST /google_auth"
(mt/with-temporary-setting-values [google-auth-client-id "pretend-client-id.apps.googleusercontent.com"]
(testing "Google auth works with an active account"
(mt/with-temp [:model/User _ {:email "test@metabase.com" :is_active true}]
(mt/with-temp [:model/User {user-id :id} {:email "test@metabase.com"
:is_active true
:first_name "last"
:last_name "luser"}]
(with-redefs [http/post (constantly
{:status 200
:body (str "{\"aud\":\"pretend-client-id.apps.googleusercontent.com\","
"\"email_verified\":\"true\","
"\"first_name\":\"test\","
"\"last_name\":\"user\","
"\"given_name\":\"test\","
"\"family_name\":\"user\","
"\"email\":\"test@metabase.com\"}")})]
(testing "with throttling enabled"
(is (malli= SessionResponse
(mt/client :post 200 "session/google_auth" {:token "foo"}))))
(mt/client :post 200 "session/google_auth" {:token "foo"})))
(is (=? {:first_name "test"
:last_name "user"}
(t2/select-one :model/User user-id))))
(testing "with throttling disabled"
(with-redefs [api.session/throttling-disabled? true]
(is (malli= SessionResponse
@@ -580,8 +586,8 @@
{:status 200
:body (str "{\"aud\":\"pretend-client-id.apps.googleusercontent.com\","
"\"email_verified\":\"true\","
"\"first_name\":\"test\","
"\"last_name\":\"user\","
"\"given_name\":\"test\","
"\"family_name\":\"user\","
"\"email\":\"test@metabase.com\"}")})]
(is (= {:errors {:_error "Your account is disabled."}}
(mt/client :post 401 "session/google_auth" {:token "foo"})))))))))