Compare commits
16 Commits
testtestte
...
debug-fail
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b83180e96 | ||
|
|
ffaca12661 | ||
|
|
6acd24fc20 | ||
|
|
2352d01b34 | ||
|
|
90b238f722 | ||
|
|
12e1e7a8ae | ||
|
|
0b42ad89a9 | ||
|
|
857bdadd45 | ||
|
|
eed6736230 | ||
|
|
144b0b33f8 | ||
|
|
dcca252247 | ||
|
|
a183e8b5b4 | ||
|
|
3b837a15a0 | ||
|
|
6679039b45 | ||
|
|
58867cac0b | ||
|
|
2e4b7adbcb |
@@ -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",
|
||||
{
|
||||
|
||||
18
.github/file-paths.yaml
vendored
18
.github/file-paths.yaml
vendored
@@ -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/**"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
31
.github/workflows/embedding-sdk-package-build.yml
vendored
Normal file
31
.github/workflows/embedding-sdk-package-build.yml
vendored
Normal 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
|
||||
93
.github/workflows/embedding-sdk.yml
vendored
93
.github/workflows/embedding-sdk.yml
vendored
@@ -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:
|
||||
|
||||
27
.github/workflows/run-tests.yml
vendored
27
.github/workflows/run-tests.yml
vendored
@@ -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]
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
61
frontend/lint/eslint-rules/no-oss-reinitialize-import.js
Normal file
61
frontend/lint/eslint-rules/no-oss-reinitialize-import.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
129
frontend/src/metabase/plugins/oss/ai.ts
Normal file
129
frontend/src/metabase/plugins/oss/ai.ts
Normal 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());
|
||||
}
|
||||
23
frontend/src/metabase/plugins/oss/api.ts
Normal file
23
frontend/src/metabase/plugins/oss/api.ts
Normal 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());
|
||||
}
|
||||
32
frontend/src/metabase/plugins/oss/audit.ts
Normal file
32
frontend/src/metabase/plugins/oss/audit.ts
Normal 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());
|
||||
}
|
||||
50
frontend/src/metabase/plugins/oss/auth.ts
Normal file
50
frontend/src/metabase/plugins/oss/auth.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
64
frontend/src/metabase/plugins/oss/caching.ts
Normal file
64
frontend/src/metabase/plugins/oss/caching.ts
Normal 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());
|
||||
}
|
||||
119
frontend/src/metabase/plugins/oss/collections.ts
Normal file
119
frontend/src/metabase/plugins/oss/collections.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
37
frontend/src/metabase/plugins/oss/content-translation.ts
Normal file
37
frontend/src/metabase/plugins/oss/content-translation.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
45
frontend/src/metabase/plugins/oss/content-verification.ts
Normal file
45
frontend/src/metabase/plugins/oss/content-verification.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
198
frontend/src/metabase/plugins/oss/core.ts
Normal file
198
frontend/src/metabase/plugins/oss/core.ts
Normal 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());
|
||||
}
|
||||
27
frontend/src/metabase/plugins/oss/dashcard-menu.ts
Normal file
27
frontend/src/metabase/plugins/oss/dashcard-menu.ts
Normal 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());
|
||||
}
|
||||
65
frontend/src/metabase/plugins/oss/database.ts
Normal file
65
frontend/src/metabase/plugins/oss/database.ts
Normal 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());
|
||||
}
|
||||
21
frontend/src/metabase/plugins/oss/documents.ts
Normal file
21
frontend/src/metabase/plugins/oss/documents.ts
Normal 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());
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
18
frontend/src/metabase/plugins/oss/embedding-iframe-sdk.ts
Normal file
18
frontend/src/metabase/plugins/oss/embedding-iframe-sdk.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
15
frontend/src/metabase/plugins/oss/embedding-sdk.ts
Normal file
15
frontend/src/metabase/plugins/oss/embedding-sdk.ts
Normal 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());
|
||||
}
|
||||
31
frontend/src/metabase/plugins/oss/embedding.ts
Normal file
31
frontend/src/metabase/plugins/oss/embedding.ts
Normal 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());
|
||||
}
|
||||
12
frontend/src/metabase/plugins/oss/entities.ts
Normal file
12
frontend/src/metabase/plugins/oss/entities.ts
Normal 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());
|
||||
}
|
||||
23
frontend/src/metabase/plugins/oss/model-persistence.ts
Normal file
23
frontend/src/metabase/plugins/oss/model-persistence.ts
Normal 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());
|
||||
}
|
||||
39
frontend/src/metabase/plugins/oss/moderation.ts
Normal file
39
frontend/src/metabase/plugins/oss/moderation.ts
Normal 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());
|
||||
}
|
||||
264
frontend/src/metabase/plugins/oss/permissions.ts
Normal file
264
frontend/src/metabase/plugins/oss/permissions.ts
Normal 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());
|
||||
}
|
||||
15
frontend/src/metabase/plugins/oss/public-sharing.ts
Normal file
15
frontend/src/metabase/plugins/oss/public-sharing.ts
Normal 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());
|
||||
}
|
||||
44
frontend/src/metabase/plugins/oss/remote-sync.ts
Normal file
44
frontend/src/metabase/plugins/oss/remote-sync.ts
Normal 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());
|
||||
}
|
||||
19
frontend/src/metabase/plugins/oss/resource-downloads.ts
Normal file
19
frontend/src/metabase/plugins/oss/resource-downloads.ts
Normal 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());
|
||||
}
|
||||
14
frontend/src/metabase/plugins/oss/semantic-search.ts
Normal file
14
frontend/src/metabase/plugins/oss/semantic-search.ts
Normal 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());
|
||||
}
|
||||
31
frontend/src/metabase/plugins/oss/settings.ts
Normal file
31
frontend/src/metabase/plugins/oss/settings.ts
Normal 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());
|
||||
}
|
||||
20
frontend/src/metabase/plugins/oss/smtp-override.ts
Normal file
20
frontend/src/metabase/plugins/oss/smtp-override.ts
Normal 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());
|
||||
}
|
||||
22
frontend/src/metabase/plugins/oss/support.ts
Normal file
22
frontend/src/metabase/plugins/oss/support.ts
Normal 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());
|
||||
}
|
||||
165
frontend/src/metabase/plugins/oss/transforms.ts
Normal file
165
frontend/src/metabase/plugins/oss/transforms.ts
Normal 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());
|
||||
}
|
||||
34
frontend/src/metabase/plugins/oss/upload-management.ts
Normal file
34
frontend/src/metabase/plugins/oss/upload-management.ts
Normal 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());
|
||||
}
|
||||
15
frontend/src/metabase/plugins/oss/whitelabel.ts
Normal file
15
frontend/src/metabase/plugins/oss/whitelabel.ts
Normal 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());
|
||||
}
|
||||
@@ -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"}}}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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"})))))))))
|
||||
|
||||
Reference in New Issue
Block a user