diff --git a/.github/actions/docker-custom-build-and-push/action.yml b/.github/actions/docker-custom-build-and-push/action.yml
index 96d4d759dbb84..bd6bb842b1fb8 100644
--- a/.github/actions/docker-custom-build-and-push/action.yml
+++ b/.github/actions/docker-custom-build-and-push/action.yml
@@ -30,6 +30,9 @@ inputs:
# e.g. latest,head,sha12345
description: "List of tags to use for the Docker image"
required: true
+ target:
+ description: "Sets the target stage to build"
+ required: false
outputs:
image_tag:
description: "Docker image tags"
@@ -62,6 +65,7 @@ runs:
platforms: linux/amd64
build-args: ${{ inputs.build-args }}
tags: ${{ steps.docker_meta.outputs.tags }}
+ target: ${{ inputs.target }}
load: true
push: false
cache-from: type=registry,ref=${{ steps.docker_meta.outputs.tags }}
@@ -94,6 +98,7 @@ runs:
platforms: ${{ inputs.platforms }}
build-args: ${{ inputs.build-args }}
tags: ${{ steps.docker_meta.outputs.tags }}
+ target: ${{ inputs.target }}
push: true
cache-from: type=registry,ref=${{ steps.docker_meta.outputs.tags }}
cache-to: type=inline
diff --git a/.github/workflows/docker-ingestion-base.yml b/.github/workflows/docker-ingestion-base.yml
index 0d29f79aa5f6c..e69de29bb2d1d 100644
--- a/.github/workflows/docker-ingestion-base.yml
+++ b/.github/workflows/docker-ingestion-base.yml
@@ -1,45 +0,0 @@
-name: ingestion base
-on:
- release:
- types: [published]
- push:
- branches:
- - master
- paths:
- - ".github/workflows/docker-ingestion-base.yml"
- - "docker/datahub-ingestion-base/**"
- - "gradle*"
- pull_request:
- branches:
- - master
- paths:
- - ".github/workflows/docker-ingestion-base.yml"
- - "docker/datahub-ingestion-base/**"
- - "gradle*"
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- build-base:
- name: Build and Push Docker Image to Docker Hub
- runs-on: ubuntu-latest
- steps:
- - name: Check out the repo
- uses: actions/checkout@v3
- with:
- fetch-depth: 800
- - name: Build and Push image
- uses: ./.github/actions/docker-custom-build-and-push
- with:
- images: |
- acryldata/datahub-ingestion-base
- tags: latest
- username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
- password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
- publish: ${{ github.ref == 'refs/heads/master' }}
- context: .
- file: ./docker/datahub-ingestion-base/Dockerfile
- platforms: linux/amd64,linux/arm64/v8
diff --git a/.github/workflows/docker-ingestion.yml b/.github/workflows/docker-ingestion.yml
deleted file mode 100644
index 26e85cfcb4a2d..0000000000000
--- a/.github/workflows/docker-ingestion.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-name: datahub-ingestion docker acryl
-on:
- push:
- branches:
- - master
- paths-ignore:
- - "docs/**"
- - "**.md"
- pull_request:
- branches:
- - master
- paths:
- - "metadata-ingestion/**"
- - "metadata-models/**"
- - "docker/datahub-ingestion/**"
- - "docker/datahub-ingestion-slim/**"
- - ".github/workflows/docker-ingestion.yml"
- release:
- types: [published]
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- setup:
- runs-on: ubuntu-latest
- outputs:
- tag: ${{ steps.tag.outputs.tag }}
- publish: ${{ steps.publish.outputs.publish }}
- python_release_version: ${{ steps.python_release_version.outputs.release_version }}
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Compute Tag
- id: tag
- run: |
- source .github/scripts/docker_helpers.sh
- echo "tag=$(get_tag)" >> $GITHUB_OUTPUT
- - name: Compute Python Release Version
- id: python_release_version
- run: |
- source .github/scripts/docker_helpers.sh
- echo "release_version=$(get_python_docker_release_v)" >> $GITHUB_OUTPUT
- - name: Check whether publishing enabled
- id: publish
- env:
- ENABLE_PUBLISH: ${{ secrets.ORG_DOCKER_PASSWORD }}
- run: |
- echo "Enable publish: ${{ env.ENABLE_PUBLISH != '' }}"
- echo "publish=${{ env.ENABLE_PUBLISH != '' }}" >> $GITHUB_OUTPUT
- push_to_registries:
- name: Build and Push Docker Image to Docker Hub
- runs-on: ubuntu-latest
- needs: setup
- steps:
- - name: Check out the repo
- uses: actions/checkout@v3
- with:
- fetch-depth: 800
- - name: Build and push
- uses: ./.github/actions/docker-custom-build-and-push
- with:
- images: |
- acryldata/datahub-ingestion
- tags: ${{ needs.setup.outputs.tag }}
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.ORG_DOCKER_PASSWORD }}
- publish: ${{ needs.setup.outputs.publish == 'true' }}
- context: .
- file: ./docker/datahub-ingestion/Dockerfile
- platforms: linux/amd64,linux/arm64/v8
- build-args: |
- RELEASE_VERSION=${{ needs.setup.outputs.python_release_version }}
- - name: Build and Push image (slim)
- uses: ./.github/actions/docker-custom-build-and-push
- with:
- images: |
- acryldata/datahub-ingestion-slim
- tags: ${{ needs.setup.outputs.tag }}
- username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
- password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
- publish: ${{ needs.setup.outputs.publish == 'true' }}
- context: .
- file: ./docker/datahub-ingestion-slim/Dockerfile
- platforms: linux/amd64,linux/arm64/v8
- ingestion-slim_scan:
- permissions:
- contents: read # for actions/checkout to fetch code
- security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
- actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
- name: "[Monitoring] Scan datahub-ingestion-slim images for vulnerabilities"
- if: ${{ github.ref == 'refs/heads/master' }}
- runs-on: ubuntu-latest
- needs: [push_to_registries]
- steps:
- - name: Checkout # adding checkout step just to make trivy upload happy
- uses: actions/checkout@v3
- - name: Download image
- uses: ishworkh/docker-image-artifact-download@v1
- with:
- image: acryldata/datahub-ingestion-slim:latest
- - name: Run Trivy vulnerability scanner
- uses: aquasecurity/trivy-action@0.8.0
- env:
- TRIVY_OFFLINE_SCAN: true
- with:
- image-ref: acryldata/datahub-ingestion-slim:latest
- format: "template"
- template: "@/contrib/sarif.tpl"
- output: "trivy-results.sarif"
- severity: "CRITICAL,HIGH"
- ignore-unfixed: true
- vuln-type: "os,library"
- - name: Upload Trivy scan results to GitHub Security tab
- uses: github/codeql-action/upload-sarif@v2
- with:
- sarif_file: "trivy-results.sarif"
diff --git a/.github/workflows/docker-unified.yml b/.github/workflows/docker-unified.yml
index 537f9cbf31d2a..c695fd98c1b46 100644
--- a/.github/workflows/docker-unified.yml
+++ b/.github/workflows/docker-unified.yml
@@ -29,6 +29,8 @@ env:
DATAHUB_MCE_CONSUMER_IMAGE: "acryldata/datahub-mce-consumer"
DATAHUB_KAFKA_SETUP_IMAGE: "acryldata/datahub-kafka-setup"
DATAHUB_ELASTIC_SETUP_IMAGE: "acryldata/datahub-elasticsearch-setup"
+ DATAHUB_INGESTION_BASE_IMAGE: "acryldata/datahub-ingestion-base"
+ DATAHUB_INGESTION_IMAGE: "acryldata/datahub-ingestion"
#### IMPORTANT ####
#### THIS IS A CHANGE TO PREVENT OSS QUICKSTART INSTABILITY, DO NOT OVERWRITE THIS CHANGE IN MERGES ####
#### IMPORTANT ####
@@ -43,7 +45,11 @@ jobs:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
+ slim_tag: ${{ steps.tag.outputs.slim_tag }}
+ full_tag: ${{ steps.tag.outputs.full_tag }}
unique_tag: ${{ steps.tag.outputs.unique_tag }}
+ unique_slim_tag: ${{ steps.tag.outputs.unique_slim_tag }}
+ unique_full_tag: ${{ steps.tag.outputs.unique_full_tag }}
publish: ${{ steps.publish.outputs.publish }}
steps:
- name: Checkout
@@ -53,14 +59,18 @@ jobs:
run: |
source .github/scripts/docker_helpers.sh
echo "tag=$(get_tag)" >> $GITHUB_OUTPUT
+ echo "slim_tag=$(get_tag)-slim" >> $GITHUB_OUTPUT
+ echo "full_tag=$(get_tag)-full" >> $GITHUB_OUTPUT
echo "unique_tag=$(get_unique_tag)" >> $GITHUB_OUTPUT
+ echo "unique_slim_tag=$(get_unique_tag)-slim" >> $GITHUB_OUTPUT
+ echo "unique_full_tag=$(get_unique_tag)-full" >> $GITHUB_OUTPUT
- name: Check whether publishing enabled
id: publish
env:
- ENABLE_PUBLISH: ${{ secrets.ORG_DOCKER_PASSWORD }}
+ ENABLE_PUBLISH: ${{ secrets.ORG_DOCKER_PASSWORD != '' && secrets.ACRYL_DOCKER_PASSWORD != '' }}
run: |
- echo "Enable publish: ${{ env.ENABLE_PUBLISH != '' }}"
- echo "publish=${{ env.ENABLE_PUBLISH != '' }}" >> $GITHUB_OUTPUT
+ echo "Enable publish: ${{ env.ENABLE_PUBLISH }}"
+ echo "publish=${{ env.ENABLE_PUBLISH }}" >> $GITHUB_OUTPUT
gms_build:
name: Build and Push DataHub GMS Docker Image
@@ -420,6 +430,289 @@ jobs:
file: ./docker/elasticsearch-setup/Dockerfile
platforms: linux/amd64,linux/arm64/v8
+ datahub_ingestion_base_build:
+ name: Build and Push DataHub Ingestion (Base) Docker Image
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ needs: setup
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 800
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ datahub-ingestion-base:
+ - 'docker/datahub-ingestion-base/**'
+ - name: Build and push Base Image
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ uses: ./.github/actions/docker-custom-build-and-push
+ with:
+ target: base
+ images: |
+ ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}
+ tags: ${{ needs.setup.outputs.tag }}
+ username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
+ password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
+ publish: ${{ needs.setup.outputs.publish }}
+ context: .
+ file: ./docker/datahub-ingestion-base/Dockerfile
+ platforms: linux/amd64,linux/arm64/v8
+ - name: Compute DataHub Ingestion (Base) Tag
+ id: tag
+ run: echo "tag=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.tag || 'head' }}" >> $GITHUB_OUTPUT
+ datahub_ingestion_base_slim_build:
+ name: Build and Push DataHub Ingestion (Base-Slim) Docker Image
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ needs: [setup, datahub_ingestion_base_build]
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 800
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ datahub-ingestion-base:
+ - 'docker/datahub-ingestion-base/**'
+ - name: Download Base Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.setup.outputs.publish != 'true' && steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_tag || 'head' }}
+ - name: Build and push Base-Slim Image
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ uses: ./.github/actions/docker-custom-build-and-push
+ with:
+ target: slim-install
+ images: |
+ ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}
+ tags: ${{ needs.setup.outputs.slim_tag }}
+ username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
+ password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
+ build-args: |
+ APP_ENV=slim
+ BASE_IMAGE=${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_tag || 'head' }}
+ publish: ${{ needs.setup.outputs.publish }}
+ context: .
+ file: ./docker/datahub-ingestion-base/Dockerfile
+ platforms: linux/amd64,linux/arm64/v8
+ - name: Compute DataHub Ingestion (Base-Slim) Tag
+ id: tag
+ run: echo "tag=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_slim_tag || 'head' }}" >> $GITHUB_OUTPUT
+ datahub_ingestion_base_full_build:
+ name: Build and Push DataHub Ingestion (Base-Full) Docker Image
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ needs: [setup, datahub_ingestion_base_build]
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 800
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ datahub-ingestion-base:
+ - 'docker/datahub-ingestion-base/**'
+ - name: Download Base Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.setup.outputs.publish != 'true' && steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_tag || 'head' }}
+ - name: Build and push Base-Full Image
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ uses: ./.github/actions/docker-custom-build-and-push
+ with:
+ target: full-install
+ images: |
+ ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}
+ tags: ${{ needs.setup.outputs.unique_full_tag }}
+ username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
+ password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
+ build-args: |
+ APP_ENV=full
+ BASE_IMAGE=${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_tag || 'head' }}
+ publish: ${{ needs.setup.outputs.publish }}
+ context: .
+ file: ./docker/datahub-ingestion-base/Dockerfile
+ platforms: linux/amd64,linux/arm64/v8
+ - name: Compute DataHub Ingestion (Base-Full) Tag
+ id: tag
+ run: echo "tag=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_full_tag || 'head' }}" >> $GITHUB_OUTPUT
+
+
+ datahub_ingestion_slim_build:
+ name: Build and Push DataHub Ingestion Docker Images
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ needs_artifact_download: ${{ (steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true') && needs.setup.outputs.publish != 'true' }}
+ needs: [setup, datahub_ingestion_base_slim_build]
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 800
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ datahub-ingestion-base:
+ - 'docker/datahub-ingestion-base/**'
+ datahub-ingestion:
+ - 'docker/datahub-ingestion/**'
+ - name: Build codegen
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true' }}
+ run: ./gradlew :metadata-ingestion:codegen
+ - name: Download Base Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.setup.outputs.publish != 'true' && steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_slim_tag || 'head' }}
+ - name: Build and push Slim Image
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true' }}
+ uses: ./.github/actions/docker-custom-build-and-push
+ with:
+ target: final
+ images: |
+ ${{ env.DATAHUB_INGESTION_IMAGE }}
+ build-args: |
+ BASE_IMAGE=${{ env.DATAHUB_INGESTION_BASE_IMAGE }}
+ DOCKER_VERSION=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_slim_tag || 'head' }}
+ APP_ENV=slim
+ tags: ${{ needs.setup.outputs.slim_tag }}
+ username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
+ password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
+ publish: ${{ needs.setup.outputs.publish }}
+ context: .
+ file: ./docker/datahub-ingestion/Dockerfile
+ platforms: linux/amd64,linux/arm64/v8
+ - name: Compute Tag
+ id: tag
+ run: echo "tag=${{ (steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true') && needs.setup.outputs.unique_slim_tag || 'head' }}" >> $GITHUB_OUTPUT
+ datahub_ingestion_slim_scan:
+ permissions:
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+ actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
+ name: "[Monitoring] Scan Datahub Ingestion Slim images for vulnerabilities"
+ runs-on: ubuntu-latest
+ needs: [setup, datahub_ingestion_slim_build]
+ steps:
+ - name: Checkout # adding checkout step just to make trivy upload happy
+ uses: actions/checkout@v3
+ - name: Download image Slim Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.datahub_ingestion_slim_build.outputs.needs_artifact_download == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_IMAGE }}:${{ needs.datahub_ingestion_slim_build.outputs.tag }}
+ - name: Run Trivy vulnerability scanner Slim Image
+ uses: aquasecurity/trivy-action@0.8.0
+ env:
+ TRIVY_OFFLINE_SCAN: true
+ with:
+ image-ref: ${{ env.DATAHUB_INGESTION_IMAGE }}:${{ needs.datahub_ingestion_slim_build.outputs.tag }}
+ format: "template"
+ template: "@/contrib/sarif.tpl"
+ output: "trivy-results.sarif"
+ severity: "CRITICAL,HIGH"
+ ignore-unfixed: true
+ vuln-type: "os,library"
+ - name: Upload Trivy scan results to GitHub Security tab
+ uses: github/codeql-action/upload-sarif@v2
+ with:
+ sarif_file: "trivy-results.sarif"
+
+ datahub_ingestion_full_build:
+ name: Build and Push DataHub Ingestion (Full) Docker Images
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ needs_artifact_download: ${{ (steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true') && needs.setup.outputs.publish != 'true' }}
+ needs: [setup, datahub_ingestion_base_full_build]
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 800
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ datahub-ingestion-base:
+ - 'docker/datahub-ingestion-base/**'
+ datahub-ingestion:
+ - 'docker/datahub-ingestion/**'
+ - name: Build codegen
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true' }}
+ run: ./gradlew :metadata-ingestion:codegen
+ - name: Download Base Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.setup.outputs.publish != 'true' && steps.filter.outputs.datahub-ingestion-base == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_BASE_IMAGE }}:${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_full_tag || 'head' }}
+ - name: Build and push Full Image
+ if: ${{ steps.filter.outputs.datahub-ingestion-base == 'true' || steps.filter.outputs.datahub-ingestion == 'true' }}
+ uses: ./.github/actions/docker-custom-build-and-push
+ with:
+ target: final
+ images: |
+ ${{ env.DATAHUB_INGESTION_IMAGE }}
+ build-args: |
+ BASE_IMAGE=${{ env.DATAHUB_INGESTION_BASE_IMAGE }}
+ DOCKER_VERSION=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_full_tag || 'head' }}
+ tags: ${{ needs.setup.outputs.unique_full_tag }}
+ username: ${{ secrets.ACRYL_DOCKER_USERNAME }}
+ password: ${{ secrets.ACRYL_DOCKER_PASSWORD }}
+ publish: ${{ needs.setup.outputs.publish }}
+ context: .
+ file: ./docker/datahub-ingestion/Dockerfile
+ platforms: linux/amd64,linux/arm64/v8
+ - name: Compute Tag (Full)
+ id: tag
+ run: echo "tag=${{ steps.filter.outputs.datahub-ingestion-base == 'true' && needs.setup.outputs.unique_full_tag || 'head' }}" >> $GITHUB_OUTPUT
+ datahub_ingestion_full_scan:
+ permissions:
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+ actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
+ name: "[Monitoring] Scan Datahub Ingestion images for vulnerabilities"
+ runs-on: ubuntu-latest
+ needs: [setup, datahub_ingestion_full_build]
+ steps:
+ - name: Checkout # adding checkout step just to make trivy upload happy
+ uses: actions/checkout@v3
+ - name: Download image Full Image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.datahub_ingestion_full_build.outputs.needs_artifact_download == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_IMAGE }}:${{ needs.datahub_ingestion_full_build.outputs.tag }}
+ - name: Run Trivy vulnerability scanner Full Image
+ uses: aquasecurity/trivy-action@0.8.0
+ env:
+ TRIVY_OFFLINE_SCAN: true
+ with:
+ image-ref: ${{ env.DATAHUB_INGESTION_IMAGE }}:${{ needs.datahub_ingestion_full_build.outputs.tag }}
+ format: "template"
+ template: "@/contrib/sarif.tpl"
+ output: "trivy-results.sarif"
+ severity: "CRITICAL,HIGH"
+ ignore-unfixed: true
+ vuln-type: "os,library"
+ - name: Upload Trivy scan results to GitHub Security tab
+ uses: github/codeql-action/upload-sarif@v2
+ with:
+ sarif_file: "trivy-results.sarif"
+
smoke_test:
name: Run Smoke Tests
runs-on: ubuntu-latest
@@ -438,8 +731,11 @@ jobs:
mae_consumer_build,
mce_consumer_build,
datahub_upgrade_build,
+ datahub_ingestion_slim_build,
]
steps:
+ - name: Disk Check
+ run: df -h . && docker images
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up JDK 11
@@ -456,6 +752,12 @@ jobs:
- name: Build datahub cli
run: |
./gradlew :metadata-ingestion:install
+ - name: Disk Check
+ run: df -h . && docker images
+ - name: Remove images
+ run: docker image prune -a -f || true
+ - name: Disk Check
+ run: df -h . && docker images
- name: Download GMS image
uses: ishworkh/docker-image-artifact-download@v1
if: ${{ needs.setup.outputs.publish != 'true' }}
@@ -496,13 +798,21 @@ jobs:
if: ${{ needs.setup.outputs.publish != 'true' }}
with:
image: ${{ env.DATAHUB_UPGRADE_IMAGE }}:${{ needs.setup.outputs.unique_tag }}
- - name: Disable datahub-actions
- run: |
- yq -i 'del(.services.datahub-actions)' docker/quickstart/docker-compose-without-neo4j.quickstart.yml
+ - name: Download datahub-ingestion-slim image
+ uses: ishworkh/docker-image-artifact-download@v1
+ if: ${{ needs.datahub_ingestion_slim_build.outputs.needs_artifact_download == 'true' }}
+ with:
+ image: ${{ env.DATAHUB_INGESTION_IMAGE }}:${{ needs.datahub_ingestion_slim_build.outputs.tag }}
+ - name: Disk Check
+ run: df -h . && docker images
- name: run quickstart
env:
DATAHUB_TELEMETRY_ENABLED: false
DATAHUB_VERSION: ${{ needs.setup.outputs.unique_tag }}
+ DATAHUB_ACTIONS_IMAGE: ${{ env.DATAHUB_INGESTION_IMAGE }}
+ ACTIONS_VERSION: ${{ needs.datahub_ingestion_slim_build.outputs.tag }}
+ ACTIONS_EXTRA_PACKAGES: 'acryl-datahub-actions[executor] acryl-datahub-actions'
+ ACTIONS_CONFIG: 'https://raw.githubusercontent.com/acryldata/datahub-actions/main/docker/config/executor.yaml'
run: |
./smoke-test/run-quickstart.sh
- name: sleep 60s
@@ -510,6 +820,8 @@ jobs:
# we are doing this because gms takes time to get ready
# and we don't have a better readiness check when bootstrap is done
sleep 60s
+ - name: Disk Check
+ run: df -h . && docker images
- name: Disable ES Disk Threshold
run: |
curl -XPUT "http://localhost:9200/_cluster/settings" \
@@ -524,6 +836,8 @@ jobs:
}'
- name: Remove Source Code
run: find ./*/* ! -path "./metadata-ingestion*" ! -path "./smoke-test*" ! -path "./gradle*" -delete
+ - name: Disk Check
+ run: df -h . && docker images
- name: Smoke test
env:
RUN_QUICKSTART: false
@@ -534,11 +848,14 @@ jobs:
run: |
echo "$DATAHUB_VERSION"
./smoke-test/smoke.sh
+ - name: Disk Check
+ run: df -h . && docker images
- name: store logs
if: failure()
run: |
docker ps -a
docker logs datahub-gms >& gms-${{ matrix.test_strategy }}.log
+ docker logs datahub-actions >& actions-${{ matrix.test_strategy }}.log
- name: Upload logs
uses: actions/upload-artifact@v3
if: failure()
diff --git a/build.gradle b/build.gradle
index 605b4fcc050e7..ae54de07cb81c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,8 +3,8 @@ buildscript {
// Releases: https://github.com/linkedin/rest.li/blob/master/CHANGELOG.md
ext.pegasusVersion = '29.22.16'
ext.mavenVersion = '3.6.3'
- ext.springVersion = '5.3.27'
- ext.springBootVersion = '2.7.11'
+ ext.springVersion = '5.3.29'
+ ext.springBootVersion = '2.7.14'
ext.openTelemetryVersion = '1.18.0'
ext.neo4jVersion = '4.4.9'
ext.testContainersVersion = '1.17.4'
@@ -18,6 +18,7 @@ buildscript {
ext.logbackClassic = '1.2.12'
ext.hadoop3Version = '3.3.5'
ext.kafkaVersion = '2.3.0'
+ ext.hazelcastVersion = '5.3.1'
ext.docker_registry = 'linkedin'
@@ -38,7 +39,7 @@ buildscript {
plugins {
id 'com.gorylenko.gradle-git-properties' version '2.4.0-rc2'
id 'com.github.johnrengelman.shadow' version '6.1.0'
- id "com.palantir.docker" version "0.34.0"
+ id "com.palantir.docker" version "0.35.0"
// https://blog.ltgt.net/javax-jakarta-mess-and-gradle-solution/
// TODO id "org.gradlex.java-ecosystem-capabilities" version "1.0"
}
@@ -101,9 +102,9 @@ project.ext.externalDependency = [
'hadoopMapreduceClient':'org.apache.hadoop:hadoop-mapreduce-client-core:2.7.2',
"hadoopClient": "org.apache.hadoop:hadoop-client:$hadoop3Version",
"hadoopCommon3":"org.apache.hadoop:hadoop-common:$hadoop3Version",
- 'hazelcast':'com.hazelcast:hazelcast:5.2.3',
- 'hazelcastSpring':'com.hazelcast:hazelcast-spring:5.2.1',
- 'hazelcastTest':'com.hazelcast:hazelcast:5.2.1:tests',
+ 'hazelcast':"com.hazelcast:hazelcast:$hazelcastVersion",
+ 'hazelcastSpring':"com.hazelcast:hazelcast-spring:$hazelcastVersion",
+ 'hazelcastTest':"com.hazelcast:hazelcast:$hazelcastVersion:tests",
'hibernateCore': 'org.hibernate:hibernate-core:5.2.16.Final',
'httpClient': 'org.apache.httpcomponents:httpclient:4.5.9',
'httpAsyncClient': 'org.apache.httpcomponents:httpasyncclient:4.1.5',
@@ -137,6 +138,7 @@ project.ext.externalDependency = [
'kafkaAvroSerde': 'io.confluent:kafka-streams-avro-serde:5.5.1',
'kafkaAvroSerializer': 'io.confluent:kafka-avro-serializer:5.1.4',
'kafkaClients': "org.apache.kafka:kafka-clients:$kafkaVersion",
+ 'snappy': 'org.xerial.snappy:snappy-java:1.1.10.3',
'logbackClassic': "ch.qos.logback:logback-classic:$logbackClassic",
'slf4jApi': "org.slf4j:slf4j-api:$slf4jVersion",
'log4jCore': "org.apache.logging.log4j:log4j-core:$log4jVersion",
diff --git a/datahub-frontend/build.gradle b/datahub-frontend/build.gradle
index f21d10d8f3842..fda33e4a9a3c6 100644
--- a/datahub-frontend/build.gradle
+++ b/datahub-frontend/build.gradle
@@ -79,6 +79,8 @@ docker {
files fileTree(rootProject.projectDir) {
include 'docker/monitoring/*'
include "docker/${docker_dir}/*"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -98,7 +100,7 @@ tasks.getByName("docker").dependsOn(unversionZip)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/datahub-frontend/play.gradle b/datahub-frontend/play.gradle
index 57f64960033aa..e7121d277926d 100644
--- a/datahub-frontend/play.gradle
+++ b/datahub-frontend/play.gradle
@@ -28,6 +28,9 @@ dependencies {
implementation(externalDependency.commonsText) {
because("previous versions are vulnerable to CVE-2022-42889")
}
+ implementation(externalDependency.snappy) {
+ because("previous versions are vulnerable to CVE-2023-34453 through CVE-2023-34455")
+ }
}
compile project(":metadata-service:restli-client")
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java
index d6dd2de6d31e3..682710ad5d539 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java
@@ -68,6 +68,7 @@
import com.linkedin.datahub.graphql.generated.ListQueriesResult;
import com.linkedin.datahub.graphql.generated.ListTestsResult;
import com.linkedin.datahub.graphql.generated.ListViewsResult;
+import com.linkedin.datahub.graphql.generated.MatchedField;
import com.linkedin.datahub.graphql.generated.MLFeature;
import com.linkedin.datahub.graphql.generated.MLFeatureProperties;
import com.linkedin.datahub.graphql.generated.MLFeatureTable;
@@ -1008,6 +1009,10 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder
.dataFetcher("entity", new EntityTypeResolver(entityTypes,
(env) -> ((SearchResult) env.getSource()).getEntity()))
)
+ .type("MatchedField", typeWiring -> typeWiring
+ .dataFetcher("entity", new EntityTypeResolver(entityTypes,
+ (env) -> ((MatchedField) env.getSource()).getEntity()))
+ )
.type("SearchAcrossLineageResult", typeWiring -> typeWiring
.dataFetcher("entity", new EntityTypeResolver(entityTypes,
(env) -> ((SearchAcrossLineageResult) env.getSource()).getEntity()))
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java
index 94880c77d74bc..3089b8c8fc2db 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java
@@ -107,7 +107,31 @@ public static boolean canEditGroupMembers(@Nonnull String groupUrnStr, @Nonnull
}
public static boolean canCreateGlobalAnnouncements(@Nonnull QueryContext context) {
- return isAuthorized(context, Optional.empty(), PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE);
+ final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(
+ ImmutableList.of(
+ new ConjunctivePrivilegeGroup(ImmutableList.of(
+ PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType())),
+ new ConjunctivePrivilegeGroup(ImmutableList.of(
+ PoliciesConfig.MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType()))
+ ));
+
+ return AuthorizationUtils.isAuthorized(
+ context.getAuthorizer(),
+ context.getActorUrn(),
+ orPrivilegeGroups);
+ }
+
+ public static boolean canManageGlobalAnnouncements(@Nonnull QueryContext context) {
+ final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(
+ ImmutableList.of(
+ new ConjunctivePrivilegeGroup(ImmutableList.of(
+ PoliciesConfig.MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType()))
+ ));
+
+ return AuthorizationUtils.isAuthorized(
+ context.getAuthorizer(),
+ context.getActorUrn(),
+ orPrivilegeGroups);
}
public static boolean canManageGlobalViews(@Nonnull QueryContext context) {
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java
index d2a7b19857f95..02921b453e315 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java
@@ -74,6 +74,7 @@ public CompletableFuture get(DataFetchingEnvironment environm
platformPrivileges.setManageTags(AuthorizationUtils.canManageTags(context));
platformPrivileges.setManageGlobalViews(AuthorizationUtils.canManageGlobalViews(context));
platformPrivileges.setManageOwnershipTypes(AuthorizationUtils.canManageOwnershipTypes(context));
+ platformPrivileges.setManageGlobalAnnouncements(AuthorizationUtils.canManageGlobalAnnouncements(context));
// Construct and return authenticated user object.
final AuthenticatedUser authUser = new AuthenticatedUser();
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java
index 2c55bc79fe501..90017f7b87997 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java
@@ -18,6 +18,7 @@
import com.linkedin.datahub.graphql.generated.Privilege;
import com.linkedin.datahub.graphql.generated.QueriesTabConfig;
import com.linkedin.datahub.graphql.generated.ResourcePrivileges;
+import com.linkedin.datahub.graphql.generated.SearchResultsVisualConfig;
import com.linkedin.datahub.graphql.generated.TelemetryConfig;
import com.linkedin.datahub.graphql.generated.TestsConfig;
import com.linkedin.datahub.graphql.generated.ViewsConfig;
@@ -144,6 +145,13 @@ public CompletableFuture get(final DataFetchingEnvironment environmen
}
visualConfig.setEntityProfiles(entityProfilesConfig);
}
+ if (_visualConfiguration != null && _visualConfiguration.getSearchResult() != null) {
+ SearchResultsVisualConfig searchResultsVisualConfig = new SearchResultsVisualConfig();
+ if (_visualConfiguration.getSearchResult().getEnableNameHighlight() != null) {
+ searchResultsVisualConfig.setEnableNameHighlight(_visualConfiguration.getSearchResult().getEnableNameHighlight());
+ }
+ visualConfig.setSearchResult(searchResultsVisualConfig);
+ }
appConfig.setVisualConfig(visualConfig);
final TelemetryConfig telemetryConfig = new TelemetryConfig();
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/post/DeletePostResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/post/DeletePostResolver.java
index cd2a3dda70033..d3cd0126fb852 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/post/DeletePostResolver.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/post/DeletePostResolver.java
@@ -23,7 +23,7 @@ public class DeletePostResolver implements DataFetcher get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
- if (!AuthorizationUtils.canCreateGlobalAnnouncements(context)) {
+ if (!AuthorizationUtils.canManageGlobalAnnouncements(context)) {
throw new AuthorizationException(
"Unauthorized to delete posts. Please contact your DataHub administrator if this needs corrective action.");
}
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java
index e40bbca56b416..fe5b79ba2ea3d 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java
@@ -73,7 +73,6 @@ private SearchUtils() {
EntityType.CONTAINER,
EntityType.DOMAIN,
EntityType.DATA_PRODUCT,
- EntityType.ROLE,
EntityType.NOTEBOOK);
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/SearchFlagsInputMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/SearchFlagsInputMapper.java
index 6435d6ee4c8e5..f3ac008734339 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/SearchFlagsInputMapper.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/SearchFlagsInputMapper.java
@@ -39,6 +39,9 @@ public com.linkedin.metadata.query.SearchFlags apply(@Nonnull final SearchFlags
if (searchFlags.getSkipAggregates() != null) {
result.setSkipAggregates(searchFlags.getSkipAggregates());
}
+ if (searchFlags.getGetSuggestions() != null) {
+ result.setGetSuggestions(searchFlags.getGetSuggestions());
+ }
return result;
}
}
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java
index 0b292a373ea40..5ba32b0c2a77c 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java
@@ -1,12 +1,18 @@
package com.linkedin.datahub.graphql.types.mappers;
+import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.generated.AggregationMetadata;
import com.linkedin.datahub.graphql.generated.FacetMetadata;
import com.linkedin.datahub.graphql.generated.MatchedField;
import com.linkedin.datahub.graphql.generated.SearchResult;
+import com.linkedin.datahub.graphql.generated.SearchSuggestion;
import com.linkedin.datahub.graphql.resolvers.EntityTypeMapper;
import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper;
import com.linkedin.metadata.search.SearchEntity;
+import com.linkedin.metadata.search.utils.SearchUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -16,6 +22,7 @@
import static com.linkedin.metadata.utils.SearchUtil.*;
+@Slf4j
public class MapperUtils {
private MapperUtils() {
@@ -54,7 +61,24 @@ public static String convertFilterValue(String filterValue, List isEnti
public static List getMatchedFieldEntry(List highlightMetadata) {
return highlightMetadata.stream()
- .map(field -> new MatchedField(field.getName(), field.getValue()))
+ .map(field -> {
+ MatchedField matchedField = new MatchedField();
+ matchedField.setName(field.getName());
+ matchedField.setValue(field.getValue());
+ if (SearchUtils.isUrn(field.getValue())) {
+ try {
+ Urn urn = Urn.createFromString(field.getValue());
+ matchedField.setEntity(UrnToEntityMapper.map(urn));
+ } catch (URISyntaxException e) {
+ log.warn("Failed to create urn from MatchedField value: {}", field.getValue(), e);
+ }
+ }
+ return matchedField;
+ })
.collect(Collectors.toList());
}
+
+ public static SearchSuggestion mapSearchSuggestion(com.linkedin.metadata.search.SearchSuggestion suggestion) {
+ return new SearchSuggestion(suggestion.getText(), suggestion.getScore(), Math.toIntExact(suggestion.getFrequency()));
+ }
}
diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/UrnSearchResultsMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/UrnSearchResultsMapper.java
index 9f750820e3093..b16e2f10d1df7 100644
--- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/UrnSearchResultsMapper.java
+++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/UrnSearchResultsMapper.java
@@ -27,6 +27,7 @@ public SearchResults apply(com.linkedin.metadata.search.SearchResult input) {
final SearchResultMetadata searchResultMetadata = input.getMetadata();
result.setSearchResults(input.getEntities().stream().map(MapperUtils::mapResult).collect(Collectors.toList()));
result.setFacets(searchResultMetadata.getAggregations().stream().map(MapperUtils::mapFacet).collect(Collectors.toList()));
+ result.setSuggestions(searchResultMetadata.getSuggestions().stream().map(MapperUtils::mapSearchSuggestion).collect(Collectors.toList()));
return result;
}
diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql
index 37183bac13f0e..dbee24b4bf6f7 100644
--- a/datahub-graphql-core/src/main/resources/app.graphql
+++ b/datahub-graphql-core/src/main/resources/app.graphql
@@ -125,6 +125,11 @@ type PlatformPrivileges {
Whether the user should be able to create, update, and delete ownership types.
"""
manageOwnershipTypes: Boolean!
+
+ """
+ Whether the user can create and delete posts pinned to the home page.
+ """
+ manageGlobalAnnouncements: Boolean!
}
"""
@@ -216,6 +221,11 @@ type VisualConfig {
Configuration for the queries tab
"""
entityProfiles: EntityProfilesConfig
+
+ """
+ Configuration for search results
+ """
+ searchResult: SearchResultsVisualConfig
}
"""
@@ -250,6 +260,16 @@ type EntityProfileConfig {
defaultTab: String
}
+"""
+Configuration for a search result
+"""
+type SearchResultsVisualConfig {
+ """
+ Whether a search result should highlight the name/description if it was matched on those fields.
+ """
+ enableNameHighlight: Boolean
+}
+
"""
Configurations related to tracking users in the app
"""
diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql
index f15535bfb4eb8..4cabdb04afe77 100644
--- a/datahub-graphql-core/src/main/resources/search.graphql
+++ b/datahub-graphql-core/src/main/resources/search.graphql
@@ -138,6 +138,11 @@ input SearchFlags {
Whether to skip aggregates/facets
"""
skipAggregates: Boolean
+
+ """
+ Whether to request for search suggestions on the _entityName virtualized field
+ """
+ getSuggestions: Boolean
}
"""
@@ -448,6 +453,11 @@ enum FilterOperator {
* Represent the relation: String field is one of the array values to, e.g. name in ["Profile", "Event"]
"""
IN
+
+ """
+ Represents the relation: The field exists. If the field is an array, the field is either not present or empty.
+ """
+ EXISTS
}
"""
@@ -478,6 +488,11 @@ type SearchResults {
Candidate facet aggregations used for search filtering
"""
facets: [FacetMetadata!]
+
+ """
+ Search suggestions based on the query provided for alternate query texts
+ """
+ suggestions: [SearchSuggestion!]
}
"""
@@ -660,6 +675,11 @@ type MatchedField {
Value of the field that matched
"""
value: String!
+
+ """
+ Entity if the value is an urn
+ """
+ entity: Entity
}
"""
@@ -717,6 +737,31 @@ type AggregationMetadata {
entity: Entity
}
+"""
+A suggestion for an alternate search query given an original query compared to all
+of the entity names in our search index.
+"""
+type SearchSuggestion {
+ """
+ The suggested text based on the provided query text compared to
+ the entity name field in the search index.
+ """
+ text: String!
+
+ """
+ The "edit distance" for this suggestion. The closer this number is to 1, the
+ closer the suggested text is to the original text. The closer it is to 0, the
+ further from the original text it is.
+ """
+ score: Float
+
+ """
+ The number of entities that would match on the name field given the suggested text
+ """
+ frequency: Int
+}
+
+
"""
Input for performing an auto completion query against a single Metadata Entity
"""
diff --git a/datahub-upgrade/build.gradle b/datahub-upgrade/build.gradle
index ad2bf02bfdcc7..78d9f6a09948d 100644
--- a/datahub-upgrade/build.gradle
+++ b/datahub-upgrade/build.gradle
@@ -89,6 +89,8 @@ docker {
files fileTree(rootProject.projectDir) {
include "docker/${docker_repo}/*"
include 'metadata-models/src/main/resources/*'
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -101,7 +103,7 @@ tasks.getByName("docker").dependsOn([bootJar])
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx
index dcefc7f70d785..a2e14308e8cee 100644
--- a/datahub-web-react/src/Mocks.tsx
+++ b/datahub-web-react/src/Mocks.tsx
@@ -1973,6 +1973,7 @@ export const mocks = [
count: 10,
filters: [],
orFilters: [],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2033,6 +2034,7 @@ export const mocks = [
],
},
],
+ suggestions: [],
},
} as GetSearchResultsQuery,
},
@@ -2059,6 +2061,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2112,6 +2115,7 @@ export const mocks = [
],
},
],
+ suggestions: [],
},
} as GetSearchResultsQuery,
},
@@ -2230,6 +2234,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2251,6 +2256,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -2772,6 +2778,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2794,6 +2801,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
__typename: 'FacetMetadata',
@@ -2886,6 +2894,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2908,6 +2917,7 @@ export const mocks = [
},
],
facets: [],
+ suggestions: [],
},
} as GetSearchResultsForMultipleQuery,
},
@@ -2934,6 +2944,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -2955,6 +2966,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -3007,6 +3019,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -3028,6 +3041,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -3084,6 +3098,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -3113,6 +3128,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -3175,6 +3191,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -3196,6 +3213,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -3258,6 +3276,7 @@ export const mocks = [
],
},
],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -3279,6 +3298,7 @@ export const mocks = [
insights: [],
},
],
+ suggestions: [],
facets: [
{
field: 'origin',
@@ -3363,6 +3383,7 @@ export const mocks = [
generatePersonalAccessTokens: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
+ manageGlobalAnnouncements: true,
},
},
},
@@ -3450,6 +3471,7 @@ export const mocks = [
count: 10,
filters: [],
orFilters: [],
+ searchFlags: { getSuggestions: true },
},
},
},
@@ -3461,6 +3483,7 @@ export const mocks = [
total: 0,
searchResults: [],
facets: [],
+ suggestions: [],
},
},
},
@@ -3609,4 +3632,5 @@ export const platformPrivileges: PlatformPrivileges = {
createDomains: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
+ manageGlobalAnnouncements: true,
};
diff --git a/datahub-web-react/src/app/entity/EntityRegistry.tsx b/datahub-web-react/src/app/entity/EntityRegistry.tsx
index a07fd02841197..56b085cf69f4a 100644
--- a/datahub-web-react/src/app/entity/EntityRegistry.tsx
+++ b/datahub-web-react/src/app/entity/EntityRegistry.tsx
@@ -1,5 +1,7 @@
+import React from 'react';
import { Entity as EntityInterface, EntityType, SearchResult } from '../../types.generated';
import { FetchedEntity } from '../lineage/types';
+import { SearchResultProvider } from '../search/context/SearchResultContext';
import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from './Entity';
import { GLOSSARY_ENTITY_TYPES } from './shared/constants';
import { GenericEntityProperties } from './shared/types';
@@ -119,7 +121,9 @@ export default class EntityRegistry {
renderSearchResult(type: EntityType, searchResult: SearchResult): JSX.Element {
const entity = validatedGet(type, this.entityTypeToEntity);
- return entity.renderSearch(searchResult);
+ return (
+ {entity.renderSearch(searchResult)}
+ );
}
renderBrowse(type: EntityType, data: T): JSX.Element {
diff --git a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx
index b5ebcbef80379..0f1b6dbf3d660 100644
--- a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx
+++ b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx
@@ -19,13 +19,14 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
import { LineageTab } from '../shared/tabs/Lineage/LineageTab';
import { ChartStatsSummarySubHeader } from './profile/stats/ChartStatsSummarySubHeader';
import { InputFieldsTab } from '../shared/tabs/Entity/InputFieldsTab';
-import { ChartSnippet } from './ChartSnippet';
import { EmbedTab } from '../shared/tabs/Embed/EmbedTab';
import { capitalizeFirstLetterOnly } from '../../shared/textUtil';
import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { getDataProduct } from '../shared/utils';
import EmbeddedProfile from '../shared/embed/EmbeddedProfile';
import { LOOKER_URN } from '../../ingest/source/builder/constants';
+import { MatchedFieldList } from '../../search/matches/MatchedFieldList';
+import { matchedInputFieldRenderer } from '../../search/matches/matchedInputFieldRenderer';
/**
* Definition of the DataHub Chart entity.
@@ -203,7 +204,11 @@ export class ChartEntity implements Entity {
lastUpdatedMs={data.properties?.lastModified?.time}
createdMs={data.properties?.created?.time}
externalUrl={data.properties?.externalUrl}
- snippet={}
+ snippet={
+ matchedInputFieldRenderer(matchedField, data)}
+ />
+ }
degree={(result as any).degree}
paths={(result as any).paths}
/>
diff --git a/datahub-web-react/src/app/entity/chart/ChartSnippet.tsx b/datahub-web-react/src/app/entity/chart/ChartSnippet.tsx
deleted file mode 100644
index 27982d3037207..0000000000000
--- a/datahub-web-react/src/app/entity/chart/ChartSnippet.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-
-import { Typography } from 'antd';
-import { InputFields, MatchedField, Maybe } from '../../../types.generated';
-import TagTermGroup from '../../shared/tags/TagTermGroup';
-import { FIELDS_TO_HIGHLIGHT } from '../dataset/search/highlights';
-import { getMatchPrioritizingPrimary } from '../shared/utils';
-
-type Props = {
- matchedFields: MatchedField[];
- inputFields: Maybe | undefined;
- isMatchingDashboard?: boolean;
-};
-
-const LABEL_INDEX_NAME = 'fieldLabels';
-const TYPE_PROPERTY_KEY_NAME = 'type';
-
-export const ChartSnippet = ({ matchedFields, inputFields, isMatchingDashboard = false }: Props) => {
- const matchedField = getMatchPrioritizingPrimary(matchedFields, 'fieldLabels');
-
- if (matchedField?.name === LABEL_INDEX_NAME) {
- const matchedSchemaField = inputFields?.fields?.find(
- (field) => field?.schemaField?.label === matchedField.value,
- );
- const matchedGlossaryTerm = matchedSchemaField?.schemaField?.glossaryTerms?.terms?.find(
- (term) => term?.term?.name === matchedField.value,
- );
-
- if (matchedGlossaryTerm) {
- let termType = 'term';
- const typeProperty = matchedGlossaryTerm.term.properties?.customProperties?.find(
- (property) => property.key === TYPE_PROPERTY_KEY_NAME,
- );
- if (typeProperty) {
- termType = typeProperty.value || termType;
- }
-
- return (
-
- Matches {termType} {' '}
- {isMatchingDashboard && 'on a contained Chart'}
-
- );
- }
- }
-
- return matchedField ? (
-
- Matches {FIELDS_TO_HIGHLIGHT.get(matchedField.name)} {matchedField.value}{' '}
- {isMatchingDashboard && 'on a contained Chart'}
-
- ) : null;
-};
diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx
index a64e437265262..0a36d0e5f1bfa 100644
--- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx
+++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx
@@ -24,12 +24,13 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
import { LineageTab } from '../shared/tabs/Lineage/LineageTab';
import { capitalizeFirstLetterOnly } from '../../shared/textUtil';
import { DashboardStatsSummarySubHeader } from './profile/DashboardStatsSummarySubHeader';
-import { ChartSnippet } from '../chart/ChartSnippet';
import { EmbedTab } from '../shared/tabs/Embed/EmbedTab';
import EmbeddedProfile from '../shared/embed/EmbeddedProfile';
import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { getDataProduct } from '../shared/utils';
import { LOOKER_URN } from '../../ingest/source/builder/constants';
+import { MatchedFieldList } from '../../search/matches/MatchedFieldList';
+import { matchedInputFieldRenderer } from '../../search/matches/matchedInputFieldRenderer';
/**
* Definition of the DataHub Dashboard entity.
@@ -227,10 +228,9 @@ export class DashboardEntity implements Entity {
lastUpdatedMs={data.properties?.lastModified?.time}
createdMs={data.properties?.created?.time}
snippet={
- matchedInputFieldRenderer(matchedField, data)}
+ matchSuffix="on a contained chart"
/>
}
subtype={data.subTypes?.typeNames?.[0]}
diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx
index cb4239872045f..ed3904bcf4e2d 100644
--- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx
+++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx
@@ -25,11 +25,12 @@ import { OperationsTab } from './profile/OperationsTab';
import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
import { SidebarSiblingsSection } from '../shared/containers/profile/sidebar/SidebarSiblingsSection';
import { DatasetStatsSummarySubHeader } from './profile/stats/stats/DatasetStatsSummarySubHeader';
-import { DatasetSearchSnippet } from './DatasetSearchSnippet';
+import { MatchedFieldList } from '../../search/matches/MatchedFieldList';
import { EmbedTab } from '../shared/tabs/Embed/EmbedTab';
import EmbeddedProfile from '../shared/embed/EmbeddedProfile';
import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { getDataProduct } from '../shared/utils';
+import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPathsRenderer';
const SUBTYPES = {
VIEW: 'view',
@@ -290,7 +291,7 @@ export class DatasetEntity implements Entity {
subtype={data.subTypes?.typeNames?.[0]}
container={data.container}
parentContainers={data.parentContainers}
- snippet={}
+ snippet={}
insights={result.insights}
externalUrl={data.properties?.externalUrl}
statsSummary={data.statsSummary}
diff --git a/datahub-web-react/src/app/entity/dataset/DatasetSearchSnippet.tsx b/datahub-web-react/src/app/entity/dataset/DatasetSearchSnippet.tsx
deleted file mode 100644
index e4f88eb0fbbfa..0000000000000
--- a/datahub-web-react/src/app/entity/dataset/DatasetSearchSnippet.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-import { Typography } from 'antd';
-import { MatchedField } from '../../../types.generated';
-import { TagSummary } from './shared/TagSummary';
-import { TermSummary } from './shared/TermSummary';
-import { FIELDS_TO_HIGHLIGHT } from './search/highlights';
-import { getMatchPrioritizingPrimary } from '../shared/utils';
-import { downgradeV2FieldPath } from './profile/schema/utils/utils';
-
-type Props = {
- matchedFields: MatchedField[];
-};
-
-const LABEL_INDEX_NAME = 'fieldLabels';
-
-export const DatasetSearchSnippet = ({ matchedFields }: Props) => {
- const matchedField = getMatchPrioritizingPrimary(matchedFields, LABEL_INDEX_NAME);
-
- let snippet: React.ReactNode;
-
- if (matchedField) {
- if (matchedField.value.includes('urn:li:tag')) {
- snippet = ;
- } else if (matchedField.value.includes('urn:li:glossaryTerm')) {
- snippet = ;
- } else if (matchedField.name === 'fieldPaths') {
- snippet = {downgradeV2FieldPath(matchedField.value)};
- } else {
- snippet = {matchedField.value};
- }
- }
-
- return matchedField ? (
-
- Matches {FIELDS_TO_HIGHLIGHT.get(matchedField.name)} {snippet}{' '}
-
- ) : null;
-};
diff --git a/datahub-web-react/src/app/entity/dataset/search/highlights.ts b/datahub-web-react/src/app/entity/dataset/search/highlights.ts
deleted file mode 100644
index 64505e0709c7b..0000000000000
--- a/datahub-web-react/src/app/entity/dataset/search/highlights.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const FIELDS_TO_HIGHLIGHT = new Map();
-FIELDS_TO_HIGHLIGHT.set('fieldPaths', 'column');
-FIELDS_TO_HIGHLIGHT.set('fieldDescriptions', 'column description');
-FIELDS_TO_HIGHLIGHT.set('fieldTags', 'column tag');
-FIELDS_TO_HIGHLIGHT.set('editedFieldDescriptions', 'column description');
-FIELDS_TO_HIGHLIGHT.set('editedFieldTags', 'column tag');
-FIELDS_TO_HIGHLIGHT.set('fieldLabels', 'label');
diff --git a/datahub-web-react/src/app/entity/dataset/shared/TagSummary.tsx b/datahub-web-react/src/app/entity/dataset/shared/TagSummary.tsx
deleted file mode 100644
index 106cc298fb58c..0000000000000
--- a/datahub-web-react/src/app/entity/dataset/shared/TagSummary.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import { useGetTagQuery } from '../../../../graphql/tag.generated';
-import { EntityType, Tag } from '../../../../types.generated';
-import { HoverEntityTooltip } from '../../../recommendations/renderer/component/HoverEntityTooltip';
-import { useEntityRegistry } from '../../../useEntityRegistry';
-import { StyledTag } from '../../shared/components/styled/StyledTag';
-
-const TagLink = styled.span`
- display: inline-block;
-`;
-
-type Props = {
- urn: string;
-};
-
-export const TagSummary = ({ urn }: Props) => {
- const entityRegistry = useEntityRegistry();
- const { data } = useGetTagQuery({ variables: { urn } });
- return (
- <>
- {data && (
-
-
-
- {entityRegistry.getDisplayName(EntityType.Tag, data?.tag)}
-
-
-
- )}
- >
- );
-};
diff --git a/datahub-web-react/src/app/entity/dataset/shared/TermSummary.tsx b/datahub-web-react/src/app/entity/dataset/shared/TermSummary.tsx
deleted file mode 100644
index cc1274693a342..0000000000000
--- a/datahub-web-react/src/app/entity/dataset/shared/TermSummary.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Tag } from 'antd';
-import { BookOutlined } from '@ant-design/icons';
-import styled from 'styled-components';
-import { useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
-import { HoverEntityTooltip } from '../../../recommendations/renderer/component/HoverEntityTooltip';
-import { EntityType, GlossaryTerm } from '../../../../types.generated';
-import { useEntityRegistry } from '../../../useEntityRegistry';
-
-const TermLink = styled.span`
- display: inline-block;
-`;
-
-type Props = {
- urn: string;
-};
-
-export const TermSummary = ({ urn }: Props) => {
- const entityRegistry = useEntityRegistry();
- const { data } = useGetGlossaryTermQuery({ variables: { urn } });
-
- return (
- <>
- {data && (
-
-
-
-
- {entityRegistry.getDisplayName(EntityType.GlossaryTerm, data?.glossaryTerm)}
-
-
-
- )}
- >
- );
-};
diff --git a/datahub-web-react/src/app/entity/glossaryTerm/preview/Preview.tsx b/datahub-web-react/src/app/entity/glossaryTerm/preview/Preview.tsx
index 26d3cf456ab7a..b6802e37652cb 100644
--- a/datahub-web-react/src/app/entity/glossaryTerm/preview/Preview.tsx
+++ b/datahub-web-react/src/app/entity/glossaryTerm/preview/Preview.tsx
@@ -4,6 +4,8 @@ import { Deprecation, Domain, EntityType, Owner, ParentNodesResult } from '../..
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { IconStyleType, PreviewType } from '../../Entity';
+import UrlButton from '../../shared/UrlButton';
+import { getRelatedEntitiesUrl } from '../utils';
export const Preview = ({
urn,
@@ -39,6 +41,9 @@ export const Preview = ({
deprecation={deprecation}
parentNodes={parentNodes}
domain={domain}
+ entityTitleSuffix={
+ View Related Entities
+ }
/>
);
};
diff --git a/datahub-web-react/src/app/entity/glossaryTerm/profile/GlossaryRelatedEntity.tsx b/datahub-web-react/src/app/entity/glossaryTerm/profile/GlossaryRelatedEntity.tsx
index d0e8de0928b48..098e97e526fd8 100644
--- a/datahub-web-react/src/app/entity/glossaryTerm/profile/GlossaryRelatedEntity.tsx
+++ b/datahub-web-react/src/app/entity/glossaryTerm/profile/GlossaryRelatedEntity.tsx
@@ -5,7 +5,7 @@ import { EmbeddedListSearchSection } from '../../shared/components/styled/search
import { useEntityData } from '../../shared/EntityContext';
export default function GlossaryRelatedEntity() {
- const { entityData }: any = useEntityData();
+ const { entityData } = useEntityData();
const entityUrn = entityData?.urn;
diff --git a/datahub-web-react/src/app/entity/glossaryTerm/utils.ts b/datahub-web-react/src/app/entity/glossaryTerm/utils.ts
index 3a2a3d35a8126..cbfa76fa34866 100644
--- a/datahub-web-react/src/app/entity/glossaryTerm/utils.ts
+++ b/datahub-web-react/src/app/entity/glossaryTerm/utils.ts
@@ -6,3 +6,7 @@ export function sortGlossaryTerms(entityRegistry: EntityRegistry, nodeA?: Entity
const nodeBName = entityRegistry.getDisplayName(EntityType.GlossaryTerm, nodeB) || '';
return nodeAName.localeCompare(nodeBName);
}
+
+export function getRelatedEntitiesUrl(entityRegistry: EntityRegistry, urn: string) {
+ return `${entityRegistry.getEntityUrl(EntityType.GlossaryTerm, urn)}/${encodeURIComponent('Related Entities')}`;
+}
diff --git a/datahub-web-react/src/app/entity/group/preview/Preview.tsx b/datahub-web-react/src/app/entity/group/preview/Preview.tsx
index dc83f6fe4f840..67449b9a481f0 100644
--- a/datahub-web-react/src/app/entity/group/preview/Preview.tsx
+++ b/datahub-web-react/src/app/entity/group/preview/Preview.tsx
@@ -8,6 +8,7 @@ import { useEntityRegistry } from '../../../useEntityRegistry';
import { ANTD_GRAY } from '../../shared/constants';
import { IconStyleType } from '../../Entity';
import NoMarkdownViewer from '../../shared/components/styled/StripMarkdownText';
+import SearchTextHighlighter from '../../../search/matches/SearchTextHighlighter';
const PreviewContainer = styled.div`
margin-bottom: 4px;
@@ -87,7 +88,9 @@ export const Preview = ({
{entityRegistry.getEntityName(EntityType.CorpGroup)}
- {name || urn}
+
+ {name ? : urn}
+
{membersCount} members
@@ -96,7 +99,12 @@ export const Preview = ({
{description && description.length > 0 && (
- {description}
+ }
+ >
+ {description}
+
)}
diff --git a/datahub-web-react/src/app/entity/shared/ExternalUrlButton.tsx b/datahub-web-react/src/app/entity/shared/ExternalUrlButton.tsx
index 9677af0776604..dce74c02cdb34 100644
--- a/datahub-web-react/src/app/entity/shared/ExternalUrlButton.tsx
+++ b/datahub-web-react/src/app/entity/shared/ExternalUrlButton.tsx
@@ -1,28 +1,11 @@
-import { ArrowRightOutlined } from '@ant-design/icons';
-import { Button } from 'antd';
import React from 'react';
-import styled from 'styled-components/macro';
import { EntityType } from '../../../types.generated';
import analytics, { EventType, EntityActionType } from '../../analytics';
+import UrlButton from './UrlButton';
const GITHUB_LINK = 'github.com';
const GITHUB = 'GitHub';
-const ExternalUrlWrapper = styled.span`
- font-size: 12px;
-`;
-
-const StyledButton = styled(Button)`
- > :hover {
- text-decoration: underline;
- }
- &&& {
- padding-bottom: 0px;
- }
- padding-left: 12px;
- padding-right: 12px;
-`;
-
interface Props {
externalUrl: string;
platformName?: string;
@@ -46,17 +29,8 @@ export default function ExternalUrlButton({ externalUrl, platformName, entityTyp
}
return (
-
-
- {displayedName ? `View in ${displayedName}` : 'View link'}{' '}
-
-
-
+
+ {displayedName ? `View in ${displayedName}` : 'View link'}
+
);
}
diff --git a/datahub-web-react/src/app/entity/shared/UrlButton.tsx b/datahub-web-react/src/app/entity/shared/UrlButton.tsx
new file mode 100644
index 0000000000000..a6f6da4a60ad5
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/UrlButton.tsx
@@ -0,0 +1,37 @@
+import React, { ReactNode } from 'react';
+import { ArrowRightOutlined } from '@ant-design/icons';
+import { Button } from 'antd';
+import styled from 'styled-components/macro';
+
+const UrlButtonContainer = styled.span`
+ font-size: 12px;
+`;
+
+const StyledButton = styled(Button)`
+ > :hover {
+ text-decoration: underline;
+ }
+ &&& {
+ padding-bottom: 0px;
+ }
+ padding-left: 12px;
+ padding-right: 12px;
+`;
+
+interface Props {
+ href: string;
+ children: ReactNode;
+ onClick?: () => void;
+}
+
+const NOOP = () => {};
+
+export default function UrlButton({ href, children, onClick = NOOP }: Props) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/datahub-web-react/src/app/entity/shared/__tests__/siblingsUtils.test.ts b/datahub-web-react/src/app/entity/shared/__tests__/siblingsUtils.test.ts
index 6e23d5400ab77..00e89e5943c17 100644
--- a/datahub-web-react/src/app/entity/shared/__tests__/siblingsUtils.test.ts
+++ b/datahub-web-react/src/app/entity/shared/__tests__/siblingsUtils.test.ts
@@ -1,10 +1,6 @@
import { dataset3WithLineage, dataset3WithSchema, dataset4WithLineage } from '../../../../Mocks';
import { EntityType, SchemaFieldDataType } from '../../../../types.generated';
-import {
- combineEntityDataWithSiblings,
- combineSiblingsInSearchResults,
- shouldEntityBeTreatedAsPrimary,
-} from '../siblingUtils';
+import { combineEntityDataWithSiblings, shouldEntityBeTreatedAsPrimary } from '../siblingUtils';
const usageStats = {
buckets: [
@@ -191,494 +187,6 @@ const datasetUnprimaryWithNoPrimarySiblings = {
},
};
-const searchResultWithSiblings = [
- {
- entity: {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
- exists: true,
- type: 'DATASET',
- name: 'cypress_project.jaffle_shop.raw_orders',
- origin: 'PROD',
- uri: null,
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- dataPlatformInstance: null,
- editableProperties: null,
- platformNativeType: null,
- properties: {
- name: 'raw_orders',
- description: null,
- qualifiedName: null,
- customProperties: [],
- __typename: 'DatasetProperties',
- },
- ownership: null,
- globalTags: null,
- glossaryTerms: null,
- subTypes: {
- typeNames: ['table'],
- __typename: 'SubTypes',
- },
- domain: null,
- container: {
- urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'jaffle_shop',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Dataset'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- parentContainers: {
- count: 2,
- containers: [
- {
- urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'jaffle_shop',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Dataset'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- {
- urn: 'urn:li:container:b5e95fce839e7d78151ed7e0a7420d84',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'cypress_project',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Project'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- ],
- __typename: 'ParentContainersResult',
- },
- deprecation: null,
- siblings: {
- isPrimary: false,
- siblings: [
- {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
- exists: true,
- type: 'DATASET',
- platform: {
- urn: 'urn:li:dataPlatform:dbt',
- type: 'DATA_PLATFORM',
- name: 'dbt',
- properties: {
- type: 'OTHERS',
- displayName: 'dbt',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/dbtlogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- name: 'cypress_project.jaffle_shop.raw_orders',
- properties: {
- name: 'raw_orders',
- description: '',
- qualifiedName: null,
- __typename: 'DatasetProperties',
- },
- __typename: 'Dataset',
- },
- ],
- __typename: 'SiblingProperties',
- },
- __typename: 'Dataset',
- },
- matchedFields: [
- {
- name: 'name',
- value: 'raw_orders',
- __typename: 'MatchedField',
- },
- {
- name: 'id',
- value: 'cypress_project.jaffle_shop.raw_orders',
- __typename: 'MatchedField',
- },
- ],
- insights: [],
- __typename: 'SearchResult',
- },
- {
- entity: {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
- exists: true,
- type: 'DATASET',
- name: 'cypress_project.jaffle_shop.raw_orders',
- origin: 'PROD',
- uri: null,
- platform: {
- urn: 'urn:li:dataPlatform:dbt',
- type: 'DATA_PLATFORM',
- name: 'dbt',
- properties: {
- type: 'OTHERS',
- displayName: 'dbt',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/dbtlogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- dataPlatformInstance: null,
- editableProperties: null,
- platformNativeType: null,
- properties: {
- name: 'raw_orders',
- description: '',
- qualifiedName: null,
- customProperties: [
- {
- key: 'catalog_version',
- value: '1.0.4',
- __typename: 'StringMapEntry',
- },
- {
- key: 'node_type',
- value: 'seed',
- __typename: 'StringMapEntry',
- },
- {
- key: 'materialization',
- value: 'seed',
- __typename: 'StringMapEntry',
- },
- {
- key: 'dbt_file_path',
- value: 'data/raw_orders.csv',
- __typename: 'StringMapEntry',
- },
- {
- key: 'catalog_schema',
- value: 'https://schemas.getdbt.com/dbt/catalog/v1.json',
- __typename: 'StringMapEntry',
- },
- {
- key: 'catalog_type',
- value: 'table',
- __typename: 'StringMapEntry',
- },
- {
- key: 'manifest_version',
- value: '1.0.4',
- __typename: 'StringMapEntry',
- },
- {
- key: 'manifest_schema',
- value: 'https://schemas.getdbt.com/dbt/manifest/v4.json',
- __typename: 'StringMapEntry',
- },
- ],
- __typename: 'DatasetProperties',
- },
- ownership: null,
- globalTags: null,
- glossaryTerms: null,
- subTypes: {
- typeNames: ['seed'],
- __typename: 'SubTypes',
- },
- domain: null,
- container: null,
- parentContainers: {
- count: 0,
- containers: [],
- __typename: 'ParentContainersResult',
- },
- deprecation: null,
- siblings: {
- isPrimary: true,
- siblings: [
- {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
- type: 'DATASET',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- name: 'cypress_project.jaffle_shop.raw_orders',
- properties: {
- name: 'raw_orders',
- description: null,
- qualifiedName: null,
- __typename: 'DatasetProperties',
- },
- __typename: 'Dataset',
- },
- ],
- __typename: 'SiblingProperties',
- },
- __typename: 'Dataset',
- },
- matchedFields: [
- {
- name: 'name',
- value: 'raw_orders',
- __typename: 'MatchedField',
- },
- {
- name: 'id',
- value: 'cypress_project.jaffle_shop.raw_orders',
- __typename: 'MatchedField',
- },
- ],
- insights: [],
- __typename: 'SearchResult',
- },
-];
-
-const searchResultWithGhostSiblings = [
- {
- entity: {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
- exists: true,
- type: 'DATASET',
- name: 'cypress_project.jaffle_shop.raw_orders',
- origin: 'PROD',
- uri: null,
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- dataPlatformInstance: null,
- editableProperties: null,
- platformNativeType: null,
- properties: {
- name: 'raw_orders',
- description: null,
- qualifiedName: null,
- customProperties: [],
- __typename: 'DatasetProperties',
- },
- ownership: null,
- globalTags: null,
- glossaryTerms: null,
- subTypes: {
- typeNames: ['table'],
- __typename: 'SubTypes',
- },
- domain: null,
- container: {
- urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'jaffle_shop',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Dataset'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- parentContainers: {
- count: 2,
- containers: [
- {
- urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'jaffle_shop',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Dataset'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- {
- urn: 'urn:li:container:b5e95fce839e7d78151ed7e0a7420d84',
- platform: {
- urn: 'urn:li:dataPlatform:bigquery',
- type: 'DATA_PLATFORM',
- name: 'bigquery',
- properties: {
- type: 'RELATIONAL_DB',
- displayName: 'BigQuery',
- datasetNameDelimiter: '.',
- logoUrl: '/assets/platforms/bigquerylogo.png',
- __typename: 'DataPlatformProperties',
- },
- displayName: null,
- info: null,
- __typename: 'DataPlatform',
- },
- properties: {
- name: 'cypress_project',
- __typename: 'ContainerProperties',
- },
- subTypes: {
- typeNames: ['Project'],
- __typename: 'SubTypes',
- },
- deprecation: null,
- __typename: 'Container',
- },
- ],
- __typename: 'ParentContainersResult',
- },
- deprecation: null,
- siblings: {
- isPrimary: false,
- siblings: [
- {
- urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
- exists: false,
- type: 'DATASET',
- },
- ],
- __typename: 'SiblingProperties',
- },
- __typename: 'Dataset',
- },
- matchedFields: [
- {
- name: 'name',
- value: 'raw_orders',
- __typename: 'MatchedField',
- },
- {
- name: 'id',
- value: 'cypress_project.jaffle_shop.raw_orders',
- __typename: 'MatchedField',
- },
- ],
- insights: [],
- __typename: 'SearchResult',
- },
-];
-
describe('siblingUtils', () => {
describe('combineEntityDataWithSiblings', () => {
it('combines my metadata with my siblings as primary', () => {
@@ -719,32 +227,6 @@ describe('siblingUtils', () => {
});
});
- describe('combineSiblingsInSearchResults', () => {
- it('combines search results to deduplicate siblings', () => {
- const result = combineSiblingsInSearchResults(searchResultWithSiblings as any);
-
- expect(result).toHaveLength(1);
- expect(result?.[0]?.matchedEntities?.[0]?.urn).toEqual(
- 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
- );
- expect(result?.[0]?.matchedEntities?.[1]?.urn).toEqual(
- 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
- );
-
- expect(result?.[0]?.matchedEntities).toHaveLength(2);
- });
-
- it('will not combine an entity with a ghost node', () => {
- const result = combineSiblingsInSearchResults(searchResultWithGhostSiblings as any);
-
- expect(result).toHaveLength(1);
- expect(result?.[0]?.matchedEntities?.[0]?.urn).toEqual(
- 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
- );
- expect(result?.[0]?.matchedEntities).toHaveLength(1);
- });
- });
-
describe('shouldEntityBeTreatedAsPrimary', () => {
it('will say a primary entity is primary', () => {
expect(shouldEntityBeTreatedAsPrimary(datasetPrimaryWithSiblings)).toBeTruthy();
diff --git a/datahub-web-react/src/app/entity/shared/__tests__/utils.test.ts b/datahub-web-react/src/app/entity/shared/__tests__/utils.test.ts
deleted file mode 100644
index 86dec46528b49..0000000000000
--- a/datahub-web-react/src/app/entity/shared/__tests__/utils.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { getMatchPrioritizingPrimary } from '../utils';
-
-const MOCK_MATCHED_FIELDS = [
- {
- name: 'fieldPaths',
- value: 'rain',
- },
- {
- name: 'description',
- value: 'rainbow',
- },
- {
- name: 'fieldPaths',
- value: 'rainbow',
- },
- {
- name: 'fieldPaths',
- value: 'rainbows',
- },
-];
-
-describe('utils', () => {
- describe('getMatchPrioritizingPrimary', () => {
- it('prioritizes exact match', () => {
- global.window.location.search = 'query=rainbow';
- const match = getMatchPrioritizingPrimary(MOCK_MATCHED_FIELDS, 'fieldPaths');
- expect(match?.value).toEqual('rainbow');
- expect(match?.name).toEqual('fieldPaths');
- });
- it('will accept first contains match', () => {
- global.window.location.search = 'query=bow';
- const match = getMatchPrioritizingPrimary(MOCK_MATCHED_FIELDS, 'fieldPaths');
- expect(match?.value).toEqual('rainbow');
- expect(match?.name).toEqual('fieldPaths');
- });
- });
-});
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/StripMarkdownText.tsx b/datahub-web-react/src/app/entity/shared/components/styled/StripMarkdownText.tsx
index 59293c2b0eee5..212813ffcb643 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/StripMarkdownText.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/StripMarkdownText.tsx
@@ -17,6 +17,7 @@ export type Props = {
suffix?: JSX.Element;
limit?: number;
shouldWrap?: boolean;
+ customRender?: (text: string) => JSX.Element;
};
export const removeMarkdown = (text: string) => {
@@ -29,7 +30,7 @@ export const removeMarkdown = (text: string) => {
.replace(/^•/, ''); // remove first •
};
-export default function NoMarkdownViewer({ children, readMore, suffix, limit, shouldWrap }: Props) {
+export default function NoMarkdownViewer({ children, customRender, readMore, suffix, limit, shouldWrap }: Props) {
let plainText = removeMarkdown(children || '');
if (limit) {
@@ -44,7 +45,8 @@ export default function NoMarkdownViewer({ children, readMore, suffix, limit, sh
return (
- {plainText} {showReadMore && <>{readMore}>} {suffix}
+ {customRender ? customRender(plainText) : plainText}
+ {showReadMore && <>{readMore}>} {suffix}
);
}
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/StyledTag.tsx b/datahub-web-react/src/app/entity/shared/components/styled/StyledTag.tsx
index c1a23811fdd7e..08087bfd79b8e 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/StyledTag.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/StyledTag.tsx
@@ -6,7 +6,15 @@ export const generateColor = new ColorHash({
saturation: 0.9,
});
-export const StyledTag = styled(Tag)<{ $color: any; $colorHash?: string; fontSize?: number }>`
+export const StyledTag = styled(Tag)<{ $color: any; $colorHash?: string; fontSize?: number; highlightTag?: boolean }>`
+ &&& {
+ ${(props) =>
+ props.highlightTag &&
+ `
+ background: ${props.theme.styles['highlight-color']};
+ border: 1px solid ${props.theme.styles['highlight-border-color']};
+ `}
+ }
${(props) => props.fontSize && `font-size: ${props.fontSize}px;`}
${(props) =>
props.$colorHash &&
diff --git a/datahub-web-react/src/app/entity/shared/constants.ts b/datahub-web-react/src/app/entity/shared/constants.ts
index e14affc95b6f9..447780fb0d641 100644
--- a/datahub-web-react/src/app/entity/shared/constants.ts
+++ b/datahub-web-react/src/app/entity/shared/constants.ts
@@ -23,6 +23,7 @@ export const ANTD_GRAY = {
export const ANTD_GRAY_V2 = {
2: '#F3F5F6',
5: '#DDE0E4',
+ 6: '#B2B8BD',
8: '#5E666E',
10: '#1B1E22',
};
diff --git a/datahub-web-react/src/app/entity/shared/siblingUtils.ts b/datahub-web-react/src/app/entity/shared/siblingUtils.ts
index 977d9fb9a9bf3..66481051055ec 100644
--- a/datahub-web-react/src/app/entity/shared/siblingUtils.ts
+++ b/datahub-web-react/src/app/entity/shared/siblingUtils.ts
@@ -2,7 +2,7 @@ import merge from 'deepmerge';
import { unionBy, keyBy, values } from 'lodash';
import { useLocation } from 'react-router-dom';
import * as QueryString from 'query-string';
-import { Dataset, Entity, MatchedField, Maybe, SiblingProperties } from '../../../types.generated';
+import { Dataset, Entity, Maybe, SiblingProperties } from '../../../types.generated';
import { GenericEntityProperties } from './types';
export function stripSiblingsFromEntity(entity: any) {
@@ -215,54 +215,48 @@ export const combineEntityDataWithSiblings = (baseEntity: T): T => {
return { [baseEntityKey]: combinedBaseEntity } as unknown as T;
};
-export type CombinedSearchResult = {
+export type CombinedEntity = {
entity: Entity;
- matchedFields: MatchedField[];
- matchedEntities?: Entity[];
+ matchedEntities?: Array;
};
-export function combineSiblingsInSearchResults(
- results:
- | {
- entity: Entity;
- matchedFields: MatchedField[];
- }[]
- | undefined,
-) {
- const combinedResults: CombinedSearchResult[] | undefined = [];
- const siblingsToPair: Record = {};
-
- // set sibling associations
- results?.forEach((result) => {
- if (result.entity.urn in siblingsToPair) {
- // filter from repeating
- // const siblingsCombinedResult = siblingsToPair[result.entity.urn];
- // siblingsCombinedResult.matchedEntities?.push(result.entity);
- return;
- }
+type CombinedEntityResult =
+ | {
+ skipped: true;
+ }
+ | {
+ skipped: false;
+ combinedEntity: CombinedEntity;
+ };
+
+export function combineSiblingsForEntity(entity: Entity, visitedSiblingUrns: Set): CombinedEntityResult {
+ if (visitedSiblingUrns.has(entity.urn)) return { skipped: true };
+
+ const combinedEntity: CombinedEntity = { entity: combineEntityWithSiblings({ ...entity }) };
+ const siblings = (combinedEntity.entity as GenericEntityProperties).siblings?.siblings ?? [];
+ const isPrimary = (combinedEntity.entity as GenericEntityProperties).siblings?.isPrimary;
+ const siblingUrns = siblings.map((sibling) => sibling?.urn);
+
+ if (siblingUrns.length > 0) {
+ combinedEntity.matchedEntities = isPrimary
+ ? [stripSiblingsFromEntity(combinedEntity.entity), ...siblings]
+ : [...siblings, stripSiblingsFromEntity(combinedEntity.entity)];
+
+ combinedEntity.matchedEntities = combinedEntity.matchedEntities.filter(
+ (resultToFilter) => (resultToFilter as Dataset).exists,
+ );
+
+ siblingUrns.forEach((urn) => urn && visitedSiblingUrns.add(urn));
+ }
- const combinedResult: CombinedSearchResult = result;
- combinedResult.entity = combineEntityWithSiblings({ ...result.entity });
- const { entity }: { entity: any } = result;
- const siblingUrns = entity?.siblings?.siblings?.map((sibling) => sibling.urn) || [];
- if (siblingUrns.length > 0) {
- combinedResult.matchedEntities = entity.siblings.isPrimary
- ? [stripSiblingsFromEntity(entity), ...entity.siblings.siblings]
- : [...entity.siblings.siblings, stripSiblingsFromEntity(entity)];
-
- combinedResult.matchedEntities = combinedResult.matchedEntities.filter(
- (resultToFilter) => (resultToFilter as Dataset).exists,
- );
-
- siblingUrns.forEach((urn) => {
- siblingsToPair[urn] = combinedResult;
- });
- }
- combinedResults.push(combinedResult);
- });
+ return { combinedEntity, skipped: false };
+}
- return combinedResults;
+export function createSiblingEntityCombiner() {
+ const visitedSiblingUrns: Set = new Set();
+ return (entity: Entity) => combineSiblingsForEntity(entity, visitedSiblingUrns);
}
+
// used to determine whether sibling entities should be shown merged or not
export const SEPARATE_SIBLINGS_URL_PARAM = 'separate_siblings';
diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx
index 1aef497ced57b..bcce994c3f0f8 100644
--- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx
+++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx
@@ -33,7 +33,7 @@ type LinkListProps = {
};
export const LinkList = ({ refetch }: LinkListProps) => {
- const { entityData } = useEntityData();
+ const { urn: entityUrn, entityData } = useEntityData();
const entityRegistry = useEntityRegistry();
const [removeLinkMutation] = useRemoveLinkMutation();
const links = entityData?.institutionalMemory?.elements || [];
@@ -41,7 +41,7 @@ export const LinkList = ({ refetch }: LinkListProps) => {
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
try {
await removeLinkMutation({
- variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn } },
+ variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn || entityUrn } },
});
message.success({ content: 'Link Removed', duration: 2 });
} catch (e: unknown) {
diff --git a/datahub-web-react/src/app/entity/shared/utils.ts b/datahub-web-react/src/app/entity/shared/utils.ts
index 7ec604785d1ff..a158cc9b7c119 100644
--- a/datahub-web-react/src/app/entity/shared/utils.ts
+++ b/datahub-web-react/src/app/entity/shared/utils.ts
@@ -1,9 +1,7 @@
-import * as QueryString from 'query-string';
import { Maybe } from 'graphql/jsutils/Maybe';
-import { Entity, EntityType, MatchedField, EntityRelationshipsResult, DataProduct } from '../../../types.generated';
+import { Entity, EntityType, EntityRelationshipsResult, DataProduct } from '../../../types.generated';
import { capitalizeFirstLetterOnly } from '../../shared/textUtil';
-import { FIELDS_TO_HIGHLIGHT } from '../dataset/search/highlights';
import { GenericEntityProperties } from './types';
export function dictToQueryStringParams(params: Record) {
@@ -87,46 +85,6 @@ export const isListSubset = (l1, l2): boolean => {
return l1.every((result) => l2.indexOf(result) >= 0);
};
-function normalize(value: string) {
- return value.trim().toLowerCase();
-}
-
-function fromQueryGetBestMatch(selectedMatchedFields: MatchedField[], rawQuery: string) {
- const query = normalize(rawQuery);
- // first lets see if there's an exact match between a field value and the query
- const exactMatch = selectedMatchedFields.find((field) => normalize(field.value) === query);
- if (exactMatch) {
- return exactMatch;
- }
-
- // if no exact match exists, we'll see if the entire query is contained in any of the values
- const containedMatch = selectedMatchedFields.find((field) => normalize(field.value).includes(query));
- if (containedMatch) {
- return containedMatch;
- }
-
- // otherwise, just return whichever is first
- return selectedMatchedFields[0];
-}
-
-export const getMatchPrioritizingPrimary = (
- matchedFields: MatchedField[],
- primaryField: string,
-): MatchedField | undefined => {
- const { location } = window;
- const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
- const query: string = decodeURIComponent(params.query ? (params.query as string) : '');
-
- const primaryMatches = matchedFields.filter((field) => field.name === primaryField);
- if (primaryMatches.length > 0) {
- return fromQueryGetBestMatch(primaryMatches, query);
- }
-
- const matchesThatShouldBeShownOnFE = matchedFields.filter((field) => FIELDS_TO_HIGHLIGHT.has(field.name));
-
- return fromQueryGetBestMatch(matchesThatShouldBeShownOnFE, query);
-};
-
function getGraphqlErrorCode(e) {
if (e.graphQLErrors && e.graphQLErrors.length) {
const firstError = e.graphQLErrors[0];
diff --git a/datahub-web-react/src/app/entity/user/preview/Preview.tsx b/datahub-web-react/src/app/entity/user/preview/Preview.tsx
index 01f68d9065523..8893d4ab86786 100644
--- a/datahub-web-react/src/app/entity/user/preview/Preview.tsx
+++ b/datahub-web-react/src/app/entity/user/preview/Preview.tsx
@@ -7,6 +7,7 @@ import { useEntityRegistry } from '../../../useEntityRegistry';
import { ANTD_GRAY } from '../../shared/constants';
import { IconStyleType } from '../../Entity';
import { CustomAvatar } from '../../../shared/avatar';
+import SearchTextHighlighter from '../../../search/matches/SearchTextHighlighter';
const PreviewContainer = styled.div`
display: flex;
@@ -80,11 +81,17 @@ export const Preview = ({
{entityRegistry.getEntityName(EntityType.CorpUser)}
- {name || urn}
+
+ {name ? : urn}
+
- {title && {title}}
+ {title && (
+
+
+
+ )}
diff --git a/datahub-web-react/src/app/entity/view/select/ViewSelect.tsx b/datahub-web-react/src/app/entity/view/select/ViewSelect.tsx
index 03689460eb02b..eda9b7d7fe2a4 100644
--- a/datahub-web-react/src/app/entity/view/select/ViewSelect.tsx
+++ b/datahub-web-react/src/app/entity/view/select/ViewSelect.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { CSSProperties, useEffect, useRef, useState } from 'react';
import { useHistory } from 'react-router';
import { Select } from 'antd';
import styled from 'styled-components';
@@ -55,11 +55,21 @@ const ViewSelectContainer = styled.div`
.ant-select-selection-item {
font-weight: 700;
font-size: 14px;
+ text-align: left;
}
}
}
`;
+const SelectStyled = styled(Select)`
+ min-width: 90px;
+ max-width: 200px;
+`;
+
+type Props = {
+ dropdownStyle?: CSSProperties;
+};
+
/**
* The View Select component allows you to select a View to apply to query on the current page. For example,
* search, recommendations, and browse.
@@ -69,7 +79,7 @@ const ViewSelectContainer = styled.div`
*
* In the event that a user refreshes their browser, the state of the view should be saved as well.
*/
-export const ViewSelect = () => {
+export const ViewSelect = ({ dropdownStyle = {} }: Props) => {
const history = useHistory();
const userContext = useUserContext();
const [isOpen, setIsOpen] = useState(false);
@@ -188,12 +198,11 @@ export const ViewSelect = () => {
return (
-
+
{viewBuilderDisplayState.visible && (
{
ref={clearButtonRef}
onClick={onHandleClickClear}
>
- All Entities
+ View all
);
diff --git a/datahub-web-react/src/app/home/HomePageHeader.tsx b/datahub-web-react/src/app/home/HomePageHeader.tsx
index def413e13213f..5919d2dbf5b7e 100644
--- a/datahub-web-react/src/app/home/HomePageHeader.tsx
+++ b/datahub-web-react/src/app/home/HomePageHeader.tsx
@@ -273,6 +273,7 @@ export const HomePageHeader = () => {
autoCompleteStyle={styles.searchBox}
entityRegistry={entityRegistry}
viewsEnabled={viewsEnabled}
+ combineSiblings
showQuickFilters
/>
{searchResultsToShow && searchResultsToShow.length > 0 && (
diff --git a/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx b/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx
index 0d8f9ddae82d1..0d0a32f7750a8 100644
--- a/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx
+++ b/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx
@@ -34,6 +34,7 @@ import ExternalUrlButton from '../entity/shared/ExternalUrlButton';
import EntityPaths from './EntityPaths/EntityPaths';
import { DataProductLink } from '../shared/tags/DataProductLink';
import { EntityHealth } from '../entity/shared/containers/profile/header/EntityHealth';
+import SearchTextHighlighter from '../search/matches/SearchTextHighlighter';
import { getUniqueOwners } from './utils';
const PreviewContainer = styled.div`
@@ -173,6 +174,7 @@ interface Props {
deprecation?: Deprecation | null;
topUsers?: Array | null;
externalUrl?: string | null;
+ entityTitleSuffix?: React.ReactNode;
subHeader?: React.ReactNode;
snippet?: React.ReactNode;
insights?: Array | null;
@@ -225,6 +227,7 @@ export default function DefaultPreviewCard({
titleSizePx,
dataTestID,
externalUrl,
+ entityTitleSuffix,
onClick,
degree,
parentContainers,
@@ -289,14 +292,14 @@ export default function DefaultPreviewCard({
) : (
- {name || ' '}
+
)}
{deprecation?.deprecated && (
)}
- {health && health.length > 0 && }
+ {health && health.length > 0 ? : null}
{externalUrl && (
)}
+ {entityTitleSuffix}
-
{degree !== undefined && degree !== null && (
) : undefined
}
+ customRender={(text) => }
>
{description}
diff --git a/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx b/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx
index e5f58a8662acc..c562fc6e8349a 100644
--- a/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx
+++ b/datahub-web-react/src/app/search/AdvancedFilterSelectValueModal.tsx
@@ -23,9 +23,7 @@ import {
REMOVED_FILTER_NAME,
TAGS_FILTER_NAME,
TYPE_NAMES_FILTER_NAME,
- DATA_PRODUCTS_FILTER_NAME,
} from './utils/constants';
-import SetDataProductModal from '../entity/shared/containers/profile/sidebar/DataProduct/SetDataProductModal';
type Props = {
facet?: FacetMetadata | null;
@@ -80,23 +78,6 @@ export const AdvancedFilterSelectValueModal = ({
);
}
- if (filterField === DATA_PRODUCTS_FILTER_NAME) {
- return (
- initialValues?.includes(agg?.entity?.urn || ''))?.entity || null
- }
- onModalClose={onCloseModal}
- onOkOverride={(dataProductUrn) => {
- onSelect([dataProductUrn]);
- onCloseModal();
- }}
- />
- );
- }
-
if (filterField === CONTAINER_FILTER_NAME) {
return (
0 ? suggestions[0].text : '';
+ const refineSearchText = getRefineSearchText(filters, viewUrn);
+
+ const onClickExploreAll = useCallback(() => {
+ analytics.event({ type: EventType.SearchResultsExploreAllClickEvent });
+ navigateToSearchUrl({ query: '*', history });
+ }, [history]);
+
+ const searchForSuggestion = () => {
+ navigateToSearchUrl({ query: suggestText, history });
+ };
+
+ const clearFiltersAndView = () => {
+ navigateToSearchUrl({ query, history });
+ userContext.updateLocalState({
+ ...userContext.localState,
+ selectedViewUrn: undefined,
+ });
+ };
+
+ return (
+
+ No results found for "{query}"
+ {refineSearchText && (
+ <>
+ Try {refineSearchText}{' '}
+ {suggestText && (
+ <>
+ or searching for {suggestText}
+ >
+ )}
+ >
+ )}
+ {!refineSearchText && suggestText && (
+ <>
+ Did you mean {suggestText}
+ >
+ )}
+ {!refineSearchText && !suggestText && (
+
+ )}
+
+ );
+}
diff --git a/datahub-web-react/src/app/search/EntityGroupSearchResults.tsx b/datahub-web-react/src/app/search/EntityGroupSearchResults.tsx
deleted file mode 100644
index 9b577048145c5..0000000000000
--- a/datahub-web-react/src/app/search/EntityGroupSearchResults.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { ArrowRightOutlined } from '@ant-design/icons';
-import { Button, Card, Divider, List, Space, Typography } from 'antd';
-import { ListProps } from 'antd/lib/list';
-import * as React from 'react';
-import { useHistory } from 'react-router-dom';
-import styled from 'styled-components';
-import { EntityType, SearchResult } from '../../types.generated';
-import { IconStyleType } from '../entity/Entity';
-import { useEntityRegistry } from '../useEntityRegistry';
-import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
-import analytics, { EventType } from '../analytics';
-
-const styles = {
- header: { marginBottom: 20 },
- resultHeaderCardBody: { padding: '16px 24px' },
- resultHeaderCard: { right: '52px', top: '-40px', position: 'absolute' },
- seeAllButton: { fontSize: 18 },
- resultsContainer: { width: '100%', padding: '40px 132px' },
-};
-
-const ResultList = styled(List)`
- &&& {
- width: 100%;
- border-color: ${(props) => props.theme.styles['border-color-base']};
- margin-top: 8px;
- padding: 16px 48px;
- box-shadow: ${(props) => props.theme.styles['box-shadow']};
- }
-`;
-
-interface Props {
- type: EntityType;
- query: string;
- searchResults: Array;
-}
-
-export const EntityGroupSearchResults = ({ type, query, searchResults }: Props) => {
- const history = useHistory();
- const entityRegistry = useEntityRegistry();
-
- const onResultClick = (result: SearchResult, index: number) => {
- analytics.event({
- type: EventType.SearchResultClickEvent,
- query,
- entityUrn: result.entity.urn,
- entityType: result.entity.type,
- index,
- total: searchResults.length,
- });
- };
-
- return (
-
- >>
- header={
-
- {entityRegistry.getCollectionName(type)}
-
- {entityRegistry.getIcon(type, 36, IconStyleType.ACCENT)}
-
-
- }
- footer={
- searchResults.length > 0 && (
-
- )
- }
- dataSource={searchResults as SearchResult[]}
- split={false}
- renderItem={(searchResult, index) => (
- <>
- onResultClick(searchResult, index)}>
- {entityRegistry.renderSearchResult(type, searchResult)}
-
- {index < searchResults.length - 1 && }
- >
- )}
- bordered
- />
-
- );
-};
diff --git a/datahub-web-react/src/app/search/PostLinkCard.tsx b/datahub-web-react/src/app/search/PostLinkCard.tsx
index 04308632c61c9..2111c0b25ad84 100644
--- a/datahub-web-react/src/app/search/PostLinkCard.tsx
+++ b/datahub-web-react/src/app/search/PostLinkCard.tsx
@@ -39,12 +39,17 @@ const TextContainer = styled.div`
flex: 2;
`;
-const TextWrapper = styled.div`
- text-align: left;
+const FlexWrapper = styled.div<{ alignCenter?: boolean }>`
display: flex;
flex-direction: column;
justify-content: center;
flex: 2;
+ ${(props) => props.alignCenter && 'align-items: center;'}
+`;
+
+const TextWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
`;
const HeaderText = styled(Typography.Text)`
@@ -74,19 +79,21 @@ export const PostLinkCard = ({ linkPost }: Props) => {
const link = linkPost?.content?.link || '';
return (
-
+
{hasMedia && (
)}
-
- Link
-
- {linkPost?.content?.title}
-
-
+
+
+ Link
+
+ {linkPost?.content?.title}
+
+
+
diff --git a/datahub-web-react/src/app/search/PostTextCard.tsx b/datahub-web-react/src/app/search/PostTextCard.tsx
index 1bba55425fe0d..15b34e37fc01c 100644
--- a/datahub-web-react/src/app/search/PostTextCard.tsx
+++ b/datahub-web-react/src/app/search/PostTextCard.tsx
@@ -7,7 +7,6 @@ import { Post } from '../../types.generated';
const CardContainer = styled.div`
display: flex;
flex-direction: row;
- min-height: 140px;
border: 1px solid ${ANTD_GRAY[4]};
border-radius: 12px;
box-shadow: ${(props) => props.theme.styles['box-shadow']};
@@ -15,6 +14,7 @@ const CardContainer = styled.div`
box-shadow: ${(props) => props.theme.styles['box-shadow-hover']};
}
white-space: unset;
+ padding-bottom: 4px;
`;
const TextContainer = styled.div`
@@ -28,6 +28,9 @@ const TextContainer = styled.div`
const TitleText = styled(Typography.Title)`
word-break: break-word;
min-height: 20px;
+ &&& {
+ margin-top: 8px;
+ }
`;
const HeaderText = styled(Typography.Text)`
diff --git a/datahub-web-react/src/app/search/SearchBar.tsx b/datahub-web-react/src/app/search/SearchBar.tsx
index 97be6ab6b65e3..fb10e1ca0026e 100644
--- a/datahub-web-react/src/app/search/SearchBar.tsx
+++ b/datahub-web-react/src/app/search/SearchBar.tsx
@@ -3,7 +3,7 @@ import { Input, AutoComplete, Button } from 'antd';
import { CloseCircleFilled, SearchOutlined } from '@ant-design/icons';
import styled from 'styled-components/macro';
import { useHistory } from 'react-router';
-import { AutoCompleteResultForEntity, Entity, EntityType, FacetFilterInput, ScenarioType } from '../../types.generated';
+import { AutoCompleteResultForEntity, EntityType, FacetFilterInput, ScenarioType } from '../../types.generated';
import EntityRegistry from '../entity/EntityRegistry';
import filterSearchQuery from './utils/filterSearchQuery';
import { ANTD_GRAY, ANTD_GRAY_V2 } from '../entity/shared/constants';
@@ -23,6 +23,7 @@ import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
import { getQuickFilterDetails } from './autoComplete/quickFilters/utils';
import ViewAllSearchItem from './ViewAllSearchItem';
import { ViewSelect } from '../entity/view/select/ViewSelect';
+import { combineSiblingsInAutoComplete } from './utils/combineSiblingsInAutoComplete';
const StyledAutoComplete = styled(AutoComplete)`
width: 100%;
@@ -88,15 +89,6 @@ const QUICK_FILTER_AUTO_COMPLETE_OPTION = {
],
};
-const renderItem = (query: string, entity: Entity) => {
- return {
- value: entity.urn,
- label: ,
- type: entity.type,
- style: { padding: '12px 12px 12px 16px' },
- };
-};
-
const renderRecommendedQuery = (query: string) => {
return {
value: query,
@@ -123,6 +115,7 @@ interface Props {
hideRecommendations?: boolean;
showQuickFilters?: boolean;
viewsEnabled?: boolean;
+ combineSiblings?: boolean;
setIsSearchBarFocused?: (isSearchBarFocused: boolean) => void;
onFocus?: () => void;
onBlur?: () => void;
@@ -149,6 +142,7 @@ export const SearchBar = ({
hideRecommendations,
showQuickFilters,
viewsEnabled = false,
+ combineSiblings = false,
setIsSearchBarFocused,
onFocus,
onBlur,
@@ -227,14 +221,26 @@ export const SearchBar = ({
];
}, [showQuickFilters, suggestions.length, effectiveQuery, selectedQuickFilter, entityRegistry]);
- const autoCompleteEntityOptions = useMemo(
- () =>
- suggestions.map((entity: AutoCompleteResultForEntity) => ({
- label: ,
- options: [...entity.entities.map((e: Entity) => renderItem(effectiveQuery, e))],
- })),
- [effectiveQuery, suggestions],
- );
+ const autoCompleteEntityOptions = useMemo(() => {
+ return suggestions.map((suggestion: AutoCompleteResultForEntity) => {
+ const combinedSuggestion = combineSiblingsInAutoComplete(suggestion, { combineSiblings });
+ return {
+ label: ,
+ options: combinedSuggestion.combinedEntities.map((combinedEntity) => ({
+ value: combinedEntity.entity.urn,
+ label: (
+
+ ),
+ type: combinedEntity.entity.type,
+ style: { padding: '12px 12px 12px 16px' },
+ })),
+ };
+ });
+ }, [combineSiblings, effectiveQuery, suggestions]);
const previousSelectedQuickFilterValue = usePrevious(selectedQuickFilter?.value);
useEffect(() => {
@@ -371,7 +377,15 @@ export const SearchBar = ({
onKeyUp={handleStopPropagation}
onKeyDown={handleStopPropagation}
>
-
+
)}
diff --git a/datahub-web-react/src/app/search/SearchPage.tsx b/datahub-web-react/src/app/search/SearchPage.tsx
index ce353640d8179..6387f0ef8c05e 100644
--- a/datahub-web-react/src/app/search/SearchPage.tsx
+++ b/datahub-web-react/src/app/search/SearchPage.tsx
@@ -59,6 +59,7 @@ export const SearchPage = () => {
orFilters,
viewUrn,
sortInput,
+ searchFlags: { getSuggestions: true },
},
},
});
@@ -235,6 +236,7 @@ export const SearchPage = () => {
error={error}
searchResponse={data?.searchAcrossEntities}
facets={data?.searchAcrossEntities?.facets}
+ suggestions={data?.searchAcrossEntities?.suggestions || []}
selectedFilters={filters}
loading={loading}
onChangeFilters={onChangeFilters}
diff --git a/datahub-web-react/src/app/search/SearchResultList.tsx b/datahub-web-react/src/app/search/SearchResultList.tsx
index c15aa15990009..386b22f34602b 100644
--- a/datahub-web-react/src/app/search/SearchResultList.tsx
+++ b/datahub-web-react/src/app/search/SearchResultList.tsx
@@ -1,17 +1,16 @@
-import React, { useCallback } from 'react';
-import { Button, Checkbox, Divider, Empty, List, ListProps } from 'antd';
+import React from 'react';
+import { Checkbox, Divider, List, ListProps } from 'antd';
import styled from 'styled-components';
-import { useHistory } from 'react-router';
-import { RocketOutlined } from '@ant-design/icons';
-import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
import { ANTD_GRAY } from '../entity/shared/constants';
-import { CombinedSearchResult, SEPARATE_SIBLINGS_URL_PARAM } from '../entity/shared/siblingUtils';
+import { SEPARATE_SIBLINGS_URL_PARAM } from '../entity/shared/siblingUtils';
import { CompactEntityNameList } from '../recommendations/renderer/component/CompactEntityNameList';
import { useEntityRegistry } from '../useEntityRegistry';
-import { SearchResult } from '../../types.generated';
+import { SearchResult, SearchSuggestion } from '../../types.generated';
import analytics, { EventType } from '../analytics';
import { EntityAndType } from '../entity/shared/types';
import { useIsSearchV2 } from './useSearchAndBrowseVersion';
+import { CombinedSearchResult } from './utils/combineSiblingsInSearchResults';
+import EmptySearchResults from './EmptySearchResults';
const ResultList = styled(List)`
&&& {
@@ -27,13 +26,6 @@ const StyledCheckbox = styled(Checkbox)`
margin-right: 12px;
`;
-const NoDataContainer = styled.div`
- > div {
- margin-top: 28px;
- margin-bottom: 28px;
- }
-`;
-
const ThinDivider = styled(Divider)`
margin-top: 16px;
margin-bottom: 16px;
@@ -69,6 +61,7 @@ type Props = {
isSelectMode: boolean;
selectedEntities: EntityAndType[];
setSelectedEntities: (entities: EntityAndType[]) => any;
+ suggestions: SearchSuggestion[];
};
export const SearchResultList = ({
@@ -78,17 +71,12 @@ export const SearchResultList = ({
isSelectMode,
selectedEntities,
setSelectedEntities,
+ suggestions,
}: Props) => {
- const history = useHistory();
const entityRegistry = useEntityRegistry();
const selectedEntityUrns = selectedEntities.map((entity) => entity.urn);
const showSearchFiltersV2 = useIsSearchV2();
- const onClickExploreAll = useCallback(() => {
- analytics.event({ type: EventType.SearchResultsExploreAllClickEvent });
- navigateToSearchUrl({ query: '*', history });
- }, [history]);
-
const onClickResult = (result: SearchResult, index: number) => {
analytics.event({
type: EventType.SearchResultClickEvent,
@@ -117,19 +105,7 @@ export const SearchResultList = ({
id="search-result-list"
dataSource={searchResults}
split={false}
- locale={{
- emptyText: (
-
-
-
-
- ),
- }}
+ locale={{ emptyText: }}
renderItem={(item, index) => (
`
display: flex;
@@ -131,6 +132,7 @@ interface Props {
setNumResultsPerPage: (numResults: number) => void;
isSelectMode: boolean;
selectedEntities: EntityAndType[];
+ suggestions: SearchSuggestion[];
setSelectedEntities: (entities: EntityAndType[]) => void;
setIsSelectMode: (showSelectMode: boolean) => any;
onChangeSelectAll: (selected: boolean) => void;
@@ -155,6 +157,7 @@ export const SearchResults = ({
setNumResultsPerPage,
isSelectMode,
selectedEntities,
+ suggestions,
setIsSelectMode,
setSelectedEntities,
onChangeSelectAll,
@@ -238,6 +241,7 @@ export const SearchResults = ({
{(error && ) ||
(!loading && (
+ {totalResults > 0 && }
-
+ {totalResults > 0 && (
+
+ )}
{authenticatedUserUrn && (
;
hasParentTooltip: boolean;
}
-export default function AutoCompleteEntity({ query, entity, hasParentTooltip }: Props) {
+export default function AutoCompleteEntity({ query, entity, siblings, hasParentTooltip }: Props) {
const entityRegistry = useEntityRegistry();
const genericEntityProps = entityRegistry.getGenericEntityProperties(entity.type, entity);
- const platformName = getPlatformName(genericEntityProps);
- const platformLogoUrl = genericEntityProps?.platform?.properties?.logoUrl;
const displayName = entityRegistry.getDisplayName(entity.type, entity);
- const icon =
- (platformLogoUrl && ) ||
- entityRegistry.getIcon(entity.type, 12, IconStyleType.ACCENT);
const { matchedText, unmatchedText } = getAutoCompleteEntityText(displayName, query);
+ const entities = siblings?.length ? siblings : [entity];
+ const platforms =
+ genericEntityProps?.siblingPlatforms
+ ?.map(
+ (platform) =>
+ getPlatformName(entityRegistry.getGenericEntityProperties(EntityType.DataPlatform, platform)) || '',
+ )
+ .filter(Boolean) ?? [];
+
const parentContainers = genericEntityProps?.parentContainers?.containers || [];
// Need to reverse parentContainers since it returns direct parent first.
const orderedParentContainers = [...parentContainers].reverse();
const subtype = genericEntityProps?.subTypes?.typeNames?.[0];
+ const showPlatforms = !!platforms.length;
+ const showPlatformDivider = !!platforms.length && !!parentContainers.length;
+ const showParentContainers = !!parentContainers.length;
+ const showHeader = showPlatforms || showParentContainers;
+
return (
- {icon}
-
+ {showHeader && (
+
+
+ {entities.map((ent) => (
+
+ ))}
+
+ {showPlatforms && }
+ {showPlatformDivider && }
+ {showParentContainers && }
+
+ )}
{
+ const entityRegistry = useEntityRegistry();
+
+ const genericEntityProps = entityRegistry.getGenericEntityProperties(entity.type, entity);
+ const platformLogoUrl = genericEntityProps?.platform?.properties?.logoUrl;
+ const platformName = getPlatformName(genericEntityProps);
+ return (
+ (platformLogoUrl && ) ||
+ entityRegistry.getIcon(entity.type, 12, IconStyleType.ACCENT)
+ );
+};
+
+export default AutoCompleteEntityIcon;
diff --git a/datahub-web-react/src/app/search/autoComplete/AutoCompleteItem.tsx b/datahub-web-react/src/app/search/autoComplete/AutoCompleteItem.tsx
index c97d171b4c931..b8f5a2c7e4081 100644
--- a/datahub-web-react/src/app/search/autoComplete/AutoCompleteItem.tsx
+++ b/datahub-web-react/src/app/search/autoComplete/AutoCompleteItem.tsx
@@ -18,9 +18,10 @@ export const SuggestionContainer = styled.div`
interface Props {
query: string;
entity: Entity;
+ siblings?: Array;
}
-export default function AutoCompleteItem({ query, entity }: Props) {
+export default function AutoCompleteItem({ query, entity, siblings }: Props) {
const entityRegistry = useEntityRegistry();
const displayTooltip = getShouldDisplayTooltip(entity, entityRegistry);
let componentToRender: React.ReactNode = null;
@@ -33,7 +34,14 @@ export default function AutoCompleteItem({ query, entity }: Props) {
componentToRender = ;
break;
default:
- componentToRender = ;
+ componentToRender = (
+
+ );
break;
}
diff --git a/datahub-web-react/src/app/search/autoComplete/AutoCompletePlatformNames.tsx b/datahub-web-react/src/app/search/autoComplete/AutoCompletePlatformNames.tsx
new file mode 100644
index 0000000000000..61fe6bcae71d0
--- /dev/null
+++ b/datahub-web-react/src/app/search/autoComplete/AutoCompletePlatformNames.tsx
@@ -0,0 +1,22 @@
+import { Typography } from 'antd';
+import React from 'react';
+import styled from 'styled-components';
+import { ANTD_GRAY_V2 } from '../../entity/shared/constants';
+
+const PlatformText = styled(Typography.Text)`
+ font-size: 12px;
+ line-height: 20px;
+ font-weight: 500;
+ color: ${ANTD_GRAY_V2[8]};
+ white-space: nowrap;
+`;
+
+type Props = {
+ platforms: Array;
+};
+
+const AutoCompletePlatformNames = ({ platforms }: Props) => {
+ return {platforms.join(' & ')};
+};
+
+export default AutoCompletePlatformNames;
diff --git a/datahub-web-react/src/app/search/autoComplete/AutoCompleteUser.tsx b/datahub-web-react/src/app/search/autoComplete/AutoCompleteUser.tsx
index 1f88b94bb0cc7..53b4d53ef46d4 100644
--- a/datahub-web-react/src/app/search/autoComplete/AutoCompleteUser.tsx
+++ b/datahub-web-react/src/app/search/autoComplete/AutoCompleteUser.tsx
@@ -1,20 +1,10 @@
import { Typography } from 'antd';
import React from 'react';
-import styled from 'styled-components';
import { CorpUser, EntityType } from '../../../types.generated';
-import { ANTD_GRAY } from '../../entity/shared/constants';
import { CustomAvatar } from '../../shared/avatar';
import { useEntityRegistry } from '../../useEntityRegistry';
import { getAutoCompleteEntityText } from './utils';
-
-export const SuggestionText = styled.div`
- margin-left: 12px;
- margin-top: 2px;
- margin-bottom: 2px;
- color: ${ANTD_GRAY[9]};
- font-size: 16px;
- overflow: hidden;
-`;
+import { SuggestionText } from './styledComponents';
interface Props {
query: string;
diff --git a/datahub-web-react/src/app/search/autoComplete/ParentContainers.tsx b/datahub-web-react/src/app/search/autoComplete/ParentContainers.tsx
index 77ccde06172c9..98a4f5aa214bb 100644
--- a/datahub-web-react/src/app/search/autoComplete/ParentContainers.tsx
+++ b/datahub-web-react/src/app/search/autoComplete/ParentContainers.tsx
@@ -4,20 +4,21 @@ import React, { Fragment } from 'react';
import styled from 'styled-components/macro';
import { Container, EntityType } from '../../../types.generated';
import { useEntityRegistry } from '../../useEntityRegistry';
-import { ANTD_GRAY } from '../../entity/shared/constants';
+import { ANTD_GRAY_V2 } from '../../entity/shared/constants';
const NUM_VISIBLE_CONTAINERS = 2;
const ParentContainersWrapper = styled.div`
font-size: 12px;
- color: ${ANTD_GRAY[9]};
+ color: ${ANTD_GRAY_V2[8]};
display: flex;
align-items: center;
- margin-bottom: 3px;
`;
const ParentContainer = styled(Typography.Text)`
+ color: ${ANTD_GRAY_V2[8]};
margin-left: 4px;
+ font-weight: 500;
`;
export const ArrowWrapper = styled.span`
diff --git a/datahub-web-react/src/app/search/autoComplete/RecommendedOption.tsx b/datahub-web-react/src/app/search/autoComplete/RecommendedOption.tsx
index 79743858b06d9..f4c31b18c99b2 100644
--- a/datahub-web-react/src/app/search/autoComplete/RecommendedOption.tsx
+++ b/datahub-web-react/src/app/search/autoComplete/RecommendedOption.tsx
@@ -1,7 +1,7 @@
import { SearchOutlined } from '@ant-design/icons';
import React from 'react';
import styled from 'styled-components/macro';
-import { SuggestionText } from './AutoCompleteUser';
+import { SuggestionText } from './styledComponents';
const TextWrapper = styled.span``;
diff --git a/datahub-web-react/src/app/search/autoComplete/styledComponents.tsx b/datahub-web-react/src/app/search/autoComplete/styledComponents.tsx
new file mode 100644
index 0000000000000..9e4b084ab3889
--- /dev/null
+++ b/datahub-web-react/src/app/search/autoComplete/styledComponents.tsx
@@ -0,0 +1,11 @@
+import styled from 'styled-components';
+import { ANTD_GRAY } from '../../entity/shared/constants';
+
+export const SuggestionText = styled.div`
+ margin-left: 12px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ color: ${ANTD_GRAY[9]};
+ font-size: 16px;
+ overflow: hidden;
+`;
diff --git a/datahub-web-react/src/app/search/context/SearchContext.tsx b/datahub-web-react/src/app/search/context/SearchContext.tsx
index ec9a0c895e876..656c57b0b22d0 100644
--- a/datahub-web-react/src/app/search/context/SearchContext.tsx
+++ b/datahub-web-react/src/app/search/context/SearchContext.tsx
@@ -1,11 +1,13 @@
import React, { useContext } from 'react';
export type SearchContextType = {
+ query: string | undefined;
selectedSortOption: string | undefined;
setSelectedSortOption: (sortOption: string) => void;
};
export const DEFAULT_CONTEXT = {
+ query: undefined,
selectedSortOption: undefined,
setSelectedSortOption: (_: string) => null,
};
@@ -21,3 +23,7 @@ export function useSearchContext() {
export function useSelectedSortOption() {
return useSearchContext().selectedSortOption;
}
+
+export function useSearchQuery() {
+ return useSearchContext().query;
+}
diff --git a/datahub-web-react/src/app/search/context/SearchContextProvider.tsx b/datahub-web-react/src/app/search/context/SearchContextProvider.tsx
index bfb65c1d74d3e..5ad9667ab1fc0 100644
--- a/datahub-web-react/src/app/search/context/SearchContextProvider.tsx
+++ b/datahub-web-react/src/app/search/context/SearchContextProvider.tsx
@@ -8,6 +8,7 @@ export default function SearchContextProvider({ children }: { children: React.Re
const history = useHistory();
const location = useLocation();
const params = useMemo(() => QueryString.parse(location.search, { arrayFormat: 'comma' }), [location.search]);
+ const query = (params.query ? decodeURIComponent(params.query as string) : undefined) as string | undefined;
const selectedSortOption = params.sortOption as string | undefined;
function setSelectedSortOption(selectedOption: string) {
@@ -15,7 +16,7 @@ export default function SearchContextProvider({ children }: { children: React.Re
}
return (
-
+
{children}
);
diff --git a/datahub-web-react/src/app/search/context/SearchResultContext.tsx b/datahub-web-react/src/app/search/context/SearchResultContext.tsx
new file mode 100644
index 0000000000000..68adead005149
--- /dev/null
+++ b/datahub-web-react/src/app/search/context/SearchResultContext.tsx
@@ -0,0 +1,72 @@
+import React, { ReactNode, createContext, useContext, useMemo } from 'react';
+import { SearchResult } from '../../../types.generated';
+import {
+ getMatchedFieldsByUrn,
+ getMatchedFieldNames,
+ getMatchedFieldsByNames,
+ shouldShowInMatchedFieldList,
+ getMatchedFieldLabel,
+ getMatchesPrioritized,
+} from '../matches/utils';
+import { MatchedFieldName } from '../matches/constants';
+
+type SearchResultContextValue = {
+ searchResult: SearchResult;
+} | null;
+
+const SearchResultContext = createContext(null);
+
+type Props = {
+ children: ReactNode;
+ searchResult: SearchResult;
+};
+
+export const SearchResultProvider = ({ children, searchResult }: Props) => {
+ const value = useMemo(
+ () => ({
+ searchResult,
+ }),
+ [searchResult],
+ );
+ return {children};
+};
+
+const useSearchResultContext = () => {
+ return useContext(SearchResultContext);
+};
+
+export const useSearchResult = () => {
+ return useSearchResultContext()?.searchResult;
+};
+
+export const useEntityType = () => {
+ return useSearchResultContext()?.searchResult.entity.type;
+};
+
+export const useMatchedFields = () => {
+ return useSearchResult()?.matchedFields ?? [];
+};
+
+export const useMatchedFieldsForList = (primaryField: MatchedFieldName) => {
+ const entityType = useEntityType();
+ const matchedFields = useMatchedFields();
+ const showableFields = matchedFields.filter((field) => shouldShowInMatchedFieldList(entityType, field));
+ return entityType ? getMatchesPrioritized(entityType, showableFields, primaryField) : [];
+};
+
+export const useMatchedFieldsByGroup = (fieldName: MatchedFieldName) => {
+ const entityType = useEntityType();
+ const matchedFields = useMatchedFields();
+ const matchedFieldNames = getMatchedFieldNames(entityType, fieldName);
+ return getMatchedFieldsByNames(matchedFields, matchedFieldNames);
+};
+
+export const useHasMatchedFieldByUrn = (urn: string, fieldName: MatchedFieldName) => {
+ const matchedFields = useMatchedFieldsByGroup(fieldName);
+ return getMatchedFieldsByUrn(matchedFields, urn).length > 0;
+};
+
+export const useMatchedFieldLabel = (fieldName: string) => {
+ const entityType = useEntityType();
+ return getMatchedFieldLabel(entityType, fieldName);
+};
diff --git a/datahub-web-react/src/app/search/context/constants.ts b/datahub-web-react/src/app/search/context/constants.ts
index 372230db023e9..5f841b8536e19 100644
--- a/datahub-web-react/src/app/search/context/constants.ts
+++ b/datahub-web-react/src/app/search/context/constants.ts
@@ -1,15 +1,23 @@
import { SortOrder } from '../../../types.generated';
export const RELEVANCE = 'relevance';
-export const NAME_FIELD = 'name';
+export const ENTITY_NAME_FIELD = '_entityName';
export const LAST_OPERATION_TIME_FIELD = 'lastOperationTime';
export const DEFAULT_SORT_OPTION = RELEVANCE;
export const SORT_OPTIONS = {
[RELEVANCE]: { label: 'Relevance', field: RELEVANCE, sortOrder: SortOrder.Descending },
- [`${NAME_FIELD}_${SortOrder.Ascending}`]: { label: 'A to Z', field: NAME_FIELD, sortOrder: SortOrder.Ascending },
- [`${NAME_FIELD}_${SortOrder.Descending}`]: { label: 'Z to A', field: NAME_FIELD, sortOrder: SortOrder.Descending },
+ [`${ENTITY_NAME_FIELD}_${SortOrder.Ascending}`]: {
+ label: 'A to Z',
+ field: ENTITY_NAME_FIELD,
+ sortOrder: SortOrder.Ascending,
+ },
+ [`${ENTITY_NAME_FIELD}_${SortOrder.Descending}`]: {
+ label: 'Z to A',
+ field: ENTITY_NAME_FIELD,
+ sortOrder: SortOrder.Descending,
+ },
[`${LAST_OPERATION_TIME_FIELD}_${SortOrder.Descending}`]: {
label: 'Last Modified in Platform',
field: LAST_OPERATION_TIME_FIELD,
diff --git a/datahub-web-react/src/app/search/matches/MatchedFieldList.tsx b/datahub-web-react/src/app/search/matches/MatchedFieldList.tsx
new file mode 100644
index 0000000000000..0bfe000dea366
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/MatchedFieldList.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+
+import { Tooltip, Typography } from 'antd';
+import styled from 'styled-components';
+import { useMatchedFieldLabel, useMatchedFieldsForList } from '../context/SearchResultContext';
+import { MatchedField } from '../../../types.generated';
+import { ANTD_GRAY_V2 } from '../../entity/shared/constants';
+import { useSearchQuery } from '../context/SearchContext';
+import { MatchesGroupedByFieldName } from './constants';
+import { useEntityRegistry } from '../../useEntityRegistry';
+import { getDescriptionSlice, isDescriptionField, isHighlightableEntityField } from './utils';
+
+const MatchesContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+`;
+
+const MatchText = styled(Typography.Text)`
+ color: ${ANTD_GRAY_V2[8]};
+ background: ${(props) => props.theme.styles['highlight-color']};
+ border-radius: 4px;
+ padding: 2px 4px 2px 4px;
+ padding-right: 4px;
+`;
+
+const MATCH_GROUP_LIMIT = 3;
+const TOOLTIP_MATCH_GROUP_LIMIT = 10;
+
+type CustomFieldRenderer = (field: MatchedField) => JSX.Element | null;
+
+type Props = {
+ customFieldRenderer?: CustomFieldRenderer;
+ matchSuffix?: string;
+};
+
+const RenderedField = ({
+ customFieldRenderer,
+ field,
+}: {
+ customFieldRenderer?: CustomFieldRenderer;
+ field: MatchedField;
+}) => {
+ const entityRegistry = useEntityRegistry();
+ const query = useSearchQuery()?.trim().toLowerCase();
+ const customRenderedField = customFieldRenderer?.(field);
+ if (customRenderedField) return {customRenderedField};
+ if (isHighlightableEntityField(field)) {
+ return field.entity ? <>{entityRegistry.getDisplayName(field.entity.type, field.entity)}> : <>>;
+ }
+ if (isDescriptionField(field) && query) return {getDescriptionSlice(field.value, query)};
+ return {field.value};
+};
+
+const MatchedFieldsList = ({
+ groupedMatch,
+ limit,
+ tooltip,
+ matchSuffix = '',
+ customFieldRenderer,
+}: {
+ groupedMatch: MatchesGroupedByFieldName;
+ limit: number;
+ tooltip?: JSX.Element;
+ matchSuffix?: string;
+ customFieldRenderer?: CustomFieldRenderer;
+}) => {
+ const label = useMatchedFieldLabel(groupedMatch.fieldName);
+ const count = groupedMatch.matchedFields.length;
+ const moreCount = Math.max(count - limit, 0);
+ const andMore = (
+ <>
+ {' '}
+ & more
+ >
+ );
+ return (
+ <>
+ Matches {count > 1 && `${count} `}
+ {label}
+ {count > 1 && 's'}{' '}
+ {groupedMatch.matchedFields.slice(0, limit).map((field, index) => (
+ <>
+ {index > 0 && ', '}
+ <>
+
+ >
+ >
+ ))}
+ {moreCount > 0 &&
+ (tooltip ? (
+
+ {andMore}
+
+ ) : (
+ <>{andMore}>
+ ))}{' '}
+ {matchSuffix}
+ >
+ );
+};
+
+export const MatchedFieldList = ({ customFieldRenderer, matchSuffix = '' }: Props) => {
+ const groupedMatches = useMatchedFieldsForList('fieldLabels');
+
+ return (
+ <>
+ {groupedMatches.length > 0 ? (
+
+ {groupedMatches.map((groupedMatch) => {
+ return (
+
+
+ }
+ />
+
+ );
+ })}
+
+ ) : null}
+ >
+ );
+};
diff --git a/datahub-web-react/src/app/search/matches/SearchTextHighlighter.tsx b/datahub-web-react/src/app/search/matches/SearchTextHighlighter.tsx
new file mode 100644
index 0000000000000..d8da1088ea89d
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/SearchTextHighlighter.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import Highlight from 'react-highlighter';
+import styled from 'styled-components';
+import { useMatchedFieldsByGroup } from '../context/SearchResultContext';
+import { useSearchQuery } from '../context/SearchContext';
+import { MatchedFieldName } from './constants';
+import { useAppConfig } from '../../useAppConfig';
+
+type Props = {
+ field: MatchedFieldName;
+ text: string;
+ enableFullHighlight?: boolean;
+};
+
+const HIGHLIGHT_ALL_PATTERN = /.*/;
+
+const StyledHighlight = styled(Highlight).attrs((props) => ({
+ matchStyle: { background: props.theme.styles['highlight-color'] },
+}))``;
+
+const SearchTextHighlighter = ({ field, text, enableFullHighlight = false }: Props) => {
+ const appConfig = useAppConfig();
+ const enableNameHighlight = appConfig.config.visualConfig.searchResult?.enableNameHighlight;
+ const matchedFields = useMatchedFieldsByGroup(field);
+ const hasMatchedField = !!matchedFields?.length;
+ const normalizedSearchQuery = useSearchQuery()?.trim().toLowerCase();
+ const normalizedText = text.trim().toLowerCase();
+ const hasSubstring = hasMatchedField && !!normalizedSearchQuery && normalizedText.includes(normalizedSearchQuery);
+ const pattern = enableFullHighlight ? HIGHLIGHT_ALL_PATTERN : undefined;
+
+ return (
+ <>
+ {enableNameHighlight && hasMatchedField ? (
+ {text}
+ ) : (
+ text
+ )}
+ >
+ );
+};
+
+export default SearchTextHighlighter;
diff --git a/datahub-web-react/src/app/search/matches/constants.ts b/datahub-web-react/src/app/search/matches/constants.ts
new file mode 100644
index 0000000000000..25ca82eef9597
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/constants.ts
@@ -0,0 +1,129 @@
+import { EntityType, MatchedField } from '../../../types.generated';
+
+export type MatchedFieldName =
+ | 'urn'
+ | 'name'
+ | 'displayName'
+ | 'title'
+ | 'description'
+ | 'editedDescription'
+ | 'editedFieldDescriptions'
+ | 'fieldDescriptions'
+ | 'tags'
+ | 'fieldTags'
+ | 'editedFieldTags'
+ | 'glossaryTerms'
+ | 'fieldGlossaryTerms'
+ | 'editedFieldGlossaryTerms'
+ | 'fieldLabels'
+ | 'fieldPaths';
+
+export type MatchedFieldConfig = {
+ name: MatchedFieldName;
+ groupInto?: MatchedFieldName;
+ label: string;
+ showInMatchedFieldList?: boolean;
+};
+
+const DEFAULT_MATCHED_FIELD_CONFIG: Array = [
+ {
+ name: 'urn',
+ label: 'urn',
+ },
+ {
+ name: 'title',
+ label: 'title',
+ },
+ {
+ name: 'displayName',
+ groupInto: 'name',
+ label: 'display name',
+ },
+ {
+ name: 'name',
+ groupInto: 'name',
+ label: 'name',
+ },
+ {
+ name: 'editedDescription',
+ groupInto: 'description',
+ label: 'description',
+ },
+ {
+ name: 'description',
+ groupInto: 'description',
+ label: 'description',
+ },
+ {
+ name: 'editedFieldDescriptions',
+ groupInto: 'fieldDescriptions',
+ label: 'column description',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'fieldDescriptions',
+ groupInto: 'fieldDescriptions',
+ label: 'column description',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'tags',
+ label: 'tag',
+ },
+ {
+ name: 'editedFieldTags',
+ groupInto: 'fieldTags',
+ label: 'column tag',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'fieldTags',
+ groupInto: 'fieldTags',
+ label: 'column tag',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'glossaryTerms',
+ label: 'term',
+ },
+ {
+ name: 'editedFieldGlossaryTerms',
+ groupInto: 'fieldGlossaryTerms',
+ label: 'column term',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'fieldGlossaryTerms',
+ groupInto: 'fieldGlossaryTerms',
+ label: 'column term',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'fieldLabels',
+ label: 'label',
+ showInMatchedFieldList: true,
+ },
+ {
+ name: 'fieldPaths',
+ label: 'column',
+ showInMatchedFieldList: true,
+ },
+];
+
+export const CHART_DASHBOARD_FIELD_CONFIG: Array = DEFAULT_MATCHED_FIELD_CONFIG.map((config) => {
+ if (config.name === 'title') return { ...config, groupInto: 'name' };
+ return config;
+});
+
+export const MATCHED_FIELD_CONFIG = {
+ [EntityType.Chart]: CHART_DASHBOARD_FIELD_CONFIG,
+ [EntityType.Dashboard]: CHART_DASHBOARD_FIELD_CONFIG,
+ DEFAULT: DEFAULT_MATCHED_FIELD_CONFIG,
+} as const;
+
+export type MatchesGroupedByFieldName = {
+ fieldName: string;
+ matchedFields: Array;
+};
+
+export const HIGHLIGHTABLE_ENTITY_TYPES = [EntityType.Tag, EntityType.GlossaryTerm];
diff --git a/datahub-web-react/src/app/search/matches/matchedFieldPathsRenderer.tsx b/datahub-web-react/src/app/search/matches/matchedFieldPathsRenderer.tsx
new file mode 100644
index 0000000000000..0a33530552864
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/matchedFieldPathsRenderer.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+
+import { MatchedField } from '../../../types.generated';
+import { downgradeV2FieldPath } from '../../entity/dataset/profile/schema/utils/utils';
+
+export const matchedFieldPathsRenderer = (matchedField: MatchedField) => {
+ return matchedField?.name === 'fieldPaths' ? {downgradeV2FieldPath(matchedField.value)} : null;
+};
diff --git a/datahub-web-react/src/app/search/matches/matchedInputFieldRenderer.tsx b/datahub-web-react/src/app/search/matches/matchedInputFieldRenderer.tsx
new file mode 100644
index 0000000000000..25634c9e8b80e
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/matchedInputFieldRenderer.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { Chart, Dashboard, EntityType, GlossaryTerm, MatchedField } from '../../../types.generated';
+import { useEntityRegistry } from '../../useEntityRegistry';
+
+const LABEL_INDEX_NAME = 'fieldLabels';
+const TYPE_PROPERTY_KEY_NAME = 'type';
+
+const TermName = ({ term }: { term: GlossaryTerm }) => {
+ const entityRegistry = useEntityRegistry();
+ return <>{entityRegistry.getDisplayName(EntityType.GlossaryTerm, term)}>;
+};
+
+export const matchedInputFieldRenderer = (matchedField: MatchedField, entity: Chart | Dashboard) => {
+ if (matchedField?.name === LABEL_INDEX_NAME) {
+ const matchedSchemaField = entity.inputFields?.fields?.find(
+ (field) => field?.schemaField?.label === matchedField.value,
+ );
+ const matchedGlossaryTerm = matchedSchemaField?.schemaField?.glossaryTerms?.terms?.find(
+ (term) => term?.term?.name === matchedField.value,
+ );
+
+ if (matchedGlossaryTerm) {
+ let termType = 'term';
+ const typeProperty = matchedGlossaryTerm.term.properties?.customProperties?.find(
+ (property) => property.key === TYPE_PROPERTY_KEY_NAME,
+ );
+ if (typeProperty) {
+ termType = typeProperty.value || termType;
+ }
+
+ return (
+ <>
+ {termType}
+ >
+ );
+ }
+ }
+ return null;
+};
diff --git a/datahub-web-react/src/app/search/matches/utils.test.ts b/datahub-web-react/src/app/search/matches/utils.test.ts
new file mode 100644
index 0000000000000..8b5ed27f5c2ad
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/utils.test.ts
@@ -0,0 +1,110 @@
+import { EntityType } from '../../../types.generated';
+import { getMatchesPrioritized } from './utils';
+
+const mapping = new Map();
+mapping.set('fieldPaths', 'column');
+mapping.set('fieldDescriptions', 'column description');
+mapping.set('fieldTags', 'column tag');
+
+const MOCK_MATCHED_FIELDS = [
+ {
+ name: 'fieldPaths',
+ value: 'rain',
+ },
+ {
+ name: 'fieldDescriptions',
+ value: 'rainbow',
+ },
+ {
+ name: 'fieldPaths',
+ value: 'rainbow',
+ },
+ {
+ name: 'fieldPaths',
+ value: 'rainbows',
+ },
+];
+
+const MOCK_MATCHED_DESCRIPTION_FIELDS = [
+ {
+ name: 'editedDescription',
+ value: 'edited description value',
+ },
+ {
+ name: 'description',
+ value: 'description value',
+ },
+ {
+ name: 'fieldDescriptions',
+ value: 'field descriptions value',
+ },
+ {
+ name: 'editedFieldDescriptions',
+ value: 'edited field descriptions value',
+ },
+];
+
+describe('utils', () => {
+ describe('getMatchPrioritizingPrimary', () => {
+ it('prioritizes exact match', () => {
+ global.window.location.search = 'query=rainbow';
+ const groupedMatches = getMatchesPrioritized(EntityType.Dataset, MOCK_MATCHED_FIELDS, 'fieldPaths');
+ expect(groupedMatches).toEqual([
+ {
+ fieldName: 'fieldPaths',
+ matchedFields: [
+ { name: 'fieldPaths', value: 'rainbow' },
+ { name: 'fieldPaths', value: 'rainbows' },
+ { name: 'fieldPaths', value: 'rain' },
+ ],
+ },
+ {
+ fieldName: 'fieldDescriptions',
+ matchedFields: [{ name: 'fieldDescriptions', value: 'rainbow' }],
+ },
+ ]);
+ });
+ it('will accept first contains match', () => {
+ global.window.location.search = 'query=bow';
+ const groupedMatches = getMatchesPrioritized(EntityType.Dataset, MOCK_MATCHED_FIELDS, 'fieldPaths');
+ expect(groupedMatches).toEqual([
+ {
+ fieldName: 'fieldPaths',
+ matchedFields: [
+ { name: 'fieldPaths', value: 'rainbow' },
+ { name: 'fieldPaths', value: 'rainbows' },
+ { name: 'fieldPaths', value: 'rain' },
+ ],
+ },
+ {
+ fieldName: 'fieldDescriptions',
+ matchedFields: [{ name: 'fieldDescriptions', value: 'rainbow' }],
+ },
+ ]);
+ });
+ it('will group by field name', () => {
+ global.window.location.search = '';
+ const groupedMatches = getMatchesPrioritized(
+ EntityType.Dataset,
+ MOCK_MATCHED_DESCRIPTION_FIELDS,
+ 'fieldPaths',
+ );
+ expect(groupedMatches).toEqual([
+ {
+ fieldName: 'description',
+ matchedFields: [
+ { name: 'editedDescription', value: 'edited description value' },
+ { name: 'description', value: 'description value' },
+ ],
+ },
+ {
+ fieldName: 'fieldDescriptions',
+ matchedFields: [
+ { name: 'fieldDescriptions', value: 'field descriptions value' },
+ { name: 'editedFieldDescriptions', value: 'edited field descriptions value' },
+ ],
+ },
+ ]);
+ });
+ });
+});
diff --git a/datahub-web-react/src/app/search/matches/utils.ts b/datahub-web-react/src/app/search/matches/utils.ts
new file mode 100644
index 0000000000000..78c62f7eef458
--- /dev/null
+++ b/datahub-web-react/src/app/search/matches/utils.ts
@@ -0,0 +1,136 @@
+import * as QueryString from 'query-string';
+import { EntityType, MatchedField } from '../../../types.generated';
+import {
+ HIGHLIGHTABLE_ENTITY_TYPES,
+ MATCHED_FIELD_CONFIG,
+ MatchedFieldConfig,
+ MatchedFieldName,
+ MatchesGroupedByFieldName,
+} from './constants';
+
+const getFieldConfigsByEntityType = (entityType: EntityType | undefined): Array => {
+ return entityType && entityType in MATCHED_FIELD_CONFIG
+ ? MATCHED_FIELD_CONFIG[entityType]
+ : MATCHED_FIELD_CONFIG.DEFAULT;
+};
+
+export const shouldShowInMatchedFieldList = (entityType: EntityType | undefined, field: MatchedField): boolean => {
+ const configs = getFieldConfigsByEntityType(entityType);
+ return configs.some((config) => config.name === field.name && config.showInMatchedFieldList);
+};
+
+export const getMatchedFieldLabel = (entityType: EntityType | undefined, fieldName: string): string => {
+ const configs = getFieldConfigsByEntityType(entityType);
+ return configs.find((config) => config.name === fieldName)?.label ?? '';
+};
+
+export const getGroupedFieldName = (
+ entityType: EntityType | undefined,
+ fieldName: string,
+): MatchedFieldName | undefined => {
+ const configs = getFieldConfigsByEntityType(entityType);
+ const fieldConfig = configs.find((config) => config.name === fieldName);
+ return fieldConfig?.groupInto;
+};
+
+export const getMatchedFieldNames = (
+ entityType: EntityType | undefined,
+ fieldName: MatchedFieldName,
+): Array => {
+ return getFieldConfigsByEntityType(entityType)
+ .filter((config) => fieldName === config.groupInto || fieldName === config.name)
+ .map((field) => field.name);
+};
+
+export const getMatchedFieldsByNames = (fields: Array, names: Array): Array => {
+ return fields.filter((field) => names.includes(field.name));
+};
+
+export const getMatchedFieldsByUrn = (fields: Array, urn: string): Array => {
+ return fields.filter((field) => field.value === urn);
+};
+
+function normalize(value: string) {
+ return value.trim().toLowerCase();
+}
+
+function fromQueryGetBestMatch(
+ selectedMatchedFields: MatchedField[],
+ rawQuery: string,
+ prioritizedField: string,
+): Array {
+ const query = normalize(rawQuery);
+ const priorityMatches: Array = selectedMatchedFields.filter(
+ (field) => field.name === prioritizedField,
+ );
+ const nonPriorityMatches: Array = selectedMatchedFields.filter(
+ (field) => field.name !== prioritizedField,
+ );
+ const exactMatches: Array = [];
+ const containedMatches: Array = [];
+ const rest: Array = [];
+
+ [...priorityMatches, ...nonPriorityMatches].forEach((field) => {
+ const normalizedValue = normalize(field.value);
+ if (normalizedValue === query) exactMatches.push(field);
+ else if (normalizedValue.includes(query)) containedMatches.push(field);
+ else rest.push(field);
+ });
+
+ return [...exactMatches, ...containedMatches, ...rest];
+}
+
+const getMatchesGroupedByFieldName = (
+ entityType: EntityType,
+ matchedFields: Array,
+): Array => {
+ const fieldNameToMatches = new Map>();
+ const fieldNames: Array = [];
+ matchedFields.forEach((field) => {
+ const groupedFieldName = getGroupedFieldName(entityType, field.name) || field.name;
+ const matchesInMap = fieldNameToMatches.get(groupedFieldName);
+ if (matchesInMap) {
+ matchesInMap.push(field);
+ } else {
+ fieldNameToMatches.set(groupedFieldName, [field]);
+ fieldNames.push(groupedFieldName);
+ }
+ });
+ return fieldNames.map((fieldName) => ({
+ fieldName,
+ matchedFields: fieldNameToMatches.get(fieldName) ?? [],
+ }));
+};
+
+export const getMatchesPrioritized = (
+ entityType: EntityType,
+ matchedFields: MatchedField[],
+ prioritizedField: string,
+): Array => {
+ const { location } = window;
+ const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
+ const query: string = decodeURIComponent(params.query ? (params.query as string) : '');
+ const matches = fromQueryGetBestMatch(matchedFields, query, prioritizedField);
+ return getMatchesGroupedByFieldName(entityType, matches);
+};
+
+export const isHighlightableEntityField = (field: MatchedField) =>
+ !!field.entity && HIGHLIGHTABLE_ENTITY_TYPES.includes(field.entity.type);
+
+export const isDescriptionField = (field: MatchedField) => field.name.toLowerCase().includes('description');
+
+const SURROUNDING_DESCRIPTION_CHARS = 10;
+const MAX_DESCRIPTION_CHARS = 50;
+
+export const getDescriptionSlice = (text: string, target: string) => {
+ const queryIndex = text.indexOf(target);
+ const start = Math.max(0, queryIndex - SURROUNDING_DESCRIPTION_CHARS);
+ const end = Math.min(
+ start + MAX_DESCRIPTION_CHARS,
+ text.length,
+ queryIndex + target.length + SURROUNDING_DESCRIPTION_CHARS,
+ );
+ const startEllipsis = start > 0 ? '...' : '';
+ const endEllipsis = end < text.length ? '...' : '';
+ return `${startEllipsis}${text.slice(start, end)}${endEllipsis}`;
+};
diff --git a/datahub-web-react/src/app/search/suggestions/SearchQuerySugggester.tsx b/datahub-web-react/src/app/search/suggestions/SearchQuerySugggester.tsx
new file mode 100644
index 0000000000000..9dbd67883bf64
--- /dev/null
+++ b/datahub-web-react/src/app/search/suggestions/SearchQuerySugggester.tsx
@@ -0,0 +1,39 @@
+import styled from 'styled-components';
+import React from 'react';
+import { useHistory } from 'react-router';
+import { SearchSuggestion } from '../../../types.generated';
+import { navigateToSearchUrl } from '../utils/navigateToSearchUrl';
+import { ANTD_GRAY_V2 } from '../../entity/shared/constants';
+
+const TextWrapper = styled.div`
+ font-size: 14px;
+ color: ${ANTD_GRAY_V2[8]};
+ margin: 16px 0 -8px 32px;
+`;
+
+export const SuggestedText = styled.span`
+ color: ${(props) => props.theme.styles['primary-color']};
+ text-decoration: underline ${(props) => props.theme.styles['primary-color']};
+ cursor: pointer;
+`;
+
+interface Props {
+ suggestions: SearchSuggestion[];
+}
+
+export default function SearchQuerySuggester({ suggestions }: Props) {
+ const history = useHistory();
+
+ if (suggestions.length === 0) return null;
+ const suggestText = suggestions[0].text;
+
+ function searchForSuggestion() {
+ navigateToSearchUrl({ query: suggestText, history });
+ }
+
+ return (
+
+ Did you mean {suggestText}
+
+ );
+}
diff --git a/datahub-web-react/src/app/search/utils/combineSiblingsInAutoComplete.ts b/datahub-web-react/src/app/search/utils/combineSiblingsInAutoComplete.ts
new file mode 100644
index 0000000000000..e8e64559e67a0
--- /dev/null
+++ b/datahub-web-react/src/app/search/utils/combineSiblingsInAutoComplete.ts
@@ -0,0 +1,31 @@
+import { AutoCompleteResultForEntity, EntityType } from '../../../types.generated';
+import { CombinedEntity, createSiblingEntityCombiner } from '../../entity/shared/siblingUtils';
+
+export type CombinedSuggestion = {
+ type: EntityType;
+ combinedEntities: Array;
+ suggestions?: AutoCompleteResultForEntity['suggestions'];
+};
+
+export function combineSiblingsInAutoComplete(
+ autoCompleteResultForEntity: AutoCompleteResultForEntity,
+ { combineSiblings = false } = {},
+): CombinedSuggestion {
+ const combine = createSiblingEntityCombiner();
+ const combinedEntities: Array = [];
+
+ autoCompleteResultForEntity.entities.forEach((entity) => {
+ if (!combineSiblings) {
+ combinedEntities.push({ entity });
+ return;
+ }
+ const combinedResult = combine(entity);
+ if (!combinedResult.skipped) combinedEntities.push(combinedResult.combinedEntity);
+ });
+
+ return {
+ type: autoCompleteResultForEntity.type,
+ suggestions: autoCompleteResultForEntity.suggestions,
+ combinedEntities,
+ };
+}
diff --git a/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.test.ts b/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.test.ts
new file mode 100644
index 0000000000000..4cf61c715b0e9
--- /dev/null
+++ b/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.test.ts
@@ -0,0 +1,521 @@
+import { combineSiblingsInSearchResults } from './combineSiblingsInSearchResults';
+
+const searchResultWithSiblings = [
+ {
+ entity: {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
+ exists: true,
+ type: 'DATASET',
+ name: 'cypress_project.jaffle_shop.raw_orders',
+ origin: 'PROD',
+ uri: null,
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ dataPlatformInstance: null,
+ editableProperties: null,
+ platformNativeType: null,
+ properties: {
+ name: 'raw_orders',
+ description: null,
+ qualifiedName: null,
+ customProperties: [],
+ __typename: 'DatasetProperties',
+ },
+ ownership: null,
+ globalTags: null,
+ glossaryTerms: null,
+ subTypes: {
+ typeNames: ['table'],
+ __typename: 'SubTypes',
+ },
+ domain: null,
+ container: {
+ urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'jaffle_shop',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Dataset'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ parentContainers: {
+ count: 2,
+ containers: [
+ {
+ urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'jaffle_shop',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Dataset'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ {
+ urn: 'urn:li:container:b5e95fce839e7d78151ed7e0a7420d84',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'cypress_project',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Project'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ ],
+ __typename: 'ParentContainersResult',
+ },
+ deprecation: null,
+ siblings: {
+ isPrimary: false,
+ siblings: [
+ {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
+ exists: true,
+ type: 'DATASET',
+ platform: {
+ urn: 'urn:li:dataPlatform:dbt',
+ type: 'DATA_PLATFORM',
+ name: 'dbt',
+ properties: {
+ type: 'OTHERS',
+ displayName: 'dbt',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/dbtlogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ name: 'cypress_project.jaffle_shop.raw_orders',
+ properties: {
+ name: 'raw_orders',
+ description: '',
+ qualifiedName: null,
+ __typename: 'DatasetProperties',
+ },
+ __typename: 'Dataset',
+ },
+ ],
+ __typename: 'SiblingProperties',
+ },
+ __typename: 'Dataset',
+ },
+ matchedFields: [
+ {
+ name: 'name',
+ value: 'raw_orders',
+ __typename: 'MatchedField',
+ },
+ {
+ name: 'id',
+ value: 'cypress_project.jaffle_shop.raw_orders',
+ __typename: 'MatchedField',
+ },
+ ],
+ insights: [],
+ __typename: 'SearchResult',
+ },
+ {
+ entity: {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
+ exists: true,
+ type: 'DATASET',
+ name: 'cypress_project.jaffle_shop.raw_orders',
+ origin: 'PROD',
+ uri: null,
+ platform: {
+ urn: 'urn:li:dataPlatform:dbt',
+ type: 'DATA_PLATFORM',
+ name: 'dbt',
+ properties: {
+ type: 'OTHERS',
+ displayName: 'dbt',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/dbtlogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ dataPlatformInstance: null,
+ editableProperties: null,
+ platformNativeType: null,
+ properties: {
+ name: 'raw_orders',
+ description: '',
+ qualifiedName: null,
+ customProperties: [
+ {
+ key: 'catalog_version',
+ value: '1.0.4',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'node_type',
+ value: 'seed',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'materialization',
+ value: 'seed',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'dbt_file_path',
+ value: 'data/raw_orders.csv',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'catalog_schema',
+ value: 'https://schemas.getdbt.com/dbt/catalog/v1.json',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'catalog_type',
+ value: 'table',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'manifest_version',
+ value: '1.0.4',
+ __typename: 'StringMapEntry',
+ },
+ {
+ key: 'manifest_schema',
+ value: 'https://schemas.getdbt.com/dbt/manifest/v4.json',
+ __typename: 'StringMapEntry',
+ },
+ ],
+ __typename: 'DatasetProperties',
+ },
+ ownership: null,
+ globalTags: null,
+ glossaryTerms: null,
+ subTypes: {
+ typeNames: ['seed'],
+ __typename: 'SubTypes',
+ },
+ domain: null,
+ container: null,
+ parentContainers: {
+ count: 0,
+ containers: [],
+ __typename: 'ParentContainersResult',
+ },
+ deprecation: null,
+ siblings: {
+ isPrimary: true,
+ siblings: [
+ {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
+ type: 'DATASET',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ name: 'cypress_project.jaffle_shop.raw_orders',
+ properties: {
+ name: 'raw_orders',
+ description: null,
+ qualifiedName: null,
+ __typename: 'DatasetProperties',
+ },
+ __typename: 'Dataset',
+ },
+ ],
+ __typename: 'SiblingProperties',
+ },
+ __typename: 'Dataset',
+ },
+ matchedFields: [
+ {
+ name: 'name',
+ value: 'raw_orders',
+ __typename: 'MatchedField',
+ },
+ {
+ name: 'id',
+ value: 'cypress_project.jaffle_shop.raw_orders',
+ __typename: 'MatchedField',
+ },
+ ],
+ insights: [],
+ __typename: 'SearchResult',
+ },
+];
+
+const searchResultWithGhostSiblings = [
+ {
+ entity: {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
+ exists: true,
+ type: 'DATASET',
+ name: 'cypress_project.jaffle_shop.raw_orders',
+ origin: 'PROD',
+ uri: null,
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ dataPlatformInstance: null,
+ editableProperties: null,
+ platformNativeType: null,
+ properties: {
+ name: 'raw_orders',
+ description: null,
+ qualifiedName: null,
+ customProperties: [],
+ __typename: 'DatasetProperties',
+ },
+ ownership: null,
+ globalTags: null,
+ glossaryTerms: null,
+ subTypes: {
+ typeNames: ['table'],
+ __typename: 'SubTypes',
+ },
+ domain: null,
+ container: {
+ urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'jaffle_shop',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Dataset'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ parentContainers: {
+ count: 2,
+ containers: [
+ {
+ urn: 'urn:li:container:348c96555971d3f5c1ffd7dd2e7446cb',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'jaffle_shop',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Dataset'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ {
+ urn: 'urn:li:container:b5e95fce839e7d78151ed7e0a7420d84',
+ platform: {
+ urn: 'urn:li:dataPlatform:bigquery',
+ type: 'DATA_PLATFORM',
+ name: 'bigquery',
+ properties: {
+ type: 'RELATIONAL_DB',
+ displayName: 'BigQuery',
+ datasetNameDelimiter: '.',
+ logoUrl: '/assets/platforms/bigquerylogo.png',
+ __typename: 'DataPlatformProperties',
+ },
+ displayName: null,
+ info: null,
+ __typename: 'DataPlatform',
+ },
+ properties: {
+ name: 'cypress_project',
+ __typename: 'ContainerProperties',
+ },
+ subTypes: {
+ typeNames: ['Project'],
+ __typename: 'SubTypes',
+ },
+ deprecation: null,
+ __typename: 'Container',
+ },
+ ],
+ __typename: 'ParentContainersResult',
+ },
+ deprecation: null,
+ siblings: {
+ isPrimary: false,
+ siblings: [
+ {
+ urn: 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
+ exists: false,
+ type: 'DATASET',
+ },
+ ],
+ __typename: 'SiblingProperties',
+ },
+ __typename: 'Dataset',
+ },
+ matchedFields: [
+ {
+ name: 'name',
+ value: 'raw_orders',
+ __typename: 'MatchedField',
+ },
+ {
+ name: 'id',
+ value: 'cypress_project.jaffle_shop.raw_orders',
+ __typename: 'MatchedField',
+ },
+ ],
+ insights: [],
+ __typename: 'SearchResult',
+ },
+];
+
+describe('siblingUtils', () => {
+ describe('combineSiblingsInSearchResults', () => {
+ it('combines search results to deduplicate siblings', () => {
+ const result = combineSiblingsInSearchResults(searchResultWithSiblings as any);
+
+ expect(result).toHaveLength(1);
+ expect(result?.[0]?.matchedEntities?.[0]?.urn).toEqual(
+ 'urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_orders,PROD)',
+ );
+ expect(result?.[0]?.matchedEntities?.[1]?.urn).toEqual(
+ 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
+ );
+
+ expect(result?.[0]?.matchedEntities).toHaveLength(2);
+
+ expect(result?.[0]?.matchedFields).toHaveLength(2);
+ });
+
+ it('will not combine an entity with a ghost node', () => {
+ const result = combineSiblingsInSearchResults(searchResultWithGhostSiblings as any);
+
+ expect(result).toHaveLength(1);
+ expect(result?.[0]?.matchedEntities?.[0]?.urn).toEqual(
+ 'urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.raw_orders,PROD)',
+ );
+ expect(result?.[0]?.matchedEntities).toHaveLength(1);
+
+ expect(result?.[0]?.matchedFields).toHaveLength(2);
+ });
+ });
+});
diff --git a/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.ts b/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.ts
new file mode 100644
index 0000000000000..4a5c8da6381b8
--- /dev/null
+++ b/datahub-web-react/src/app/search/utils/combineSiblingsInSearchResults.ts
@@ -0,0 +1,28 @@
+import { Entity, MatchedField } from '../../../types.generated';
+import { CombinedEntity, createSiblingEntityCombiner } from '../../entity/shared/siblingUtils';
+
+type UncombinedSeaerchResults = {
+ entity: Entity;
+ matchedFields: Array;
+};
+
+export type CombinedSearchResult = CombinedEntity & Pick;
+
+export function combineSiblingsInSearchResults(
+ searchResults: Array | undefined = [],
+): Array {
+ const combine = createSiblingEntityCombiner();
+ const combinedSearchResults: Array = [];
+
+ searchResults.forEach((searchResult) => {
+ const combinedResult = combine(searchResult.entity);
+ if (!combinedResult.skipped) {
+ combinedSearchResults.push({
+ ...searchResult,
+ ...combinedResult.combinedEntity,
+ });
+ }
+ });
+
+ return combinedSearchResults;
+}
diff --git a/datahub-web-react/src/app/search/utils/constants.ts b/datahub-web-react/src/app/search/utils/constants.ts
index eecd18441e7a5..af45129022cc1 100644
--- a/datahub-web-react/src/app/search/utils/constants.ts
+++ b/datahub-web-react/src/app/search/utils/constants.ts
@@ -10,7 +10,6 @@ export const TAGS_FILTER_NAME = 'tags';
export const GLOSSARY_TERMS_FILTER_NAME = 'glossaryTerms';
export const CONTAINER_FILTER_NAME = 'container';
export const DOMAINS_FILTER_NAME = 'domains';
-export const DATA_PRODUCTS_FILTER_NAME = 'dataProducts';
export const OWNERS_FILTER_NAME = 'owners';
export const TYPE_NAMES_FILTER_NAME = 'typeNames';
export const PLATFORM_FILTER_NAME = 'platform';
@@ -57,7 +56,6 @@ export const ORDERED_FIELDS = [
TAGS_FILTER_NAME,
GLOSSARY_TERMS_FILTER_NAME,
DOMAINS_FILTER_NAME,
- DATA_PRODUCTS_FILTER_NAME,
FIELD_TAGS_FILTER_NAME,
FIELD_GLOSSARY_TERMS_FILTER_NAME,
FIELD_PATHS_FILTER_NAME,
@@ -74,7 +72,6 @@ export const FIELD_TO_LABEL = {
owners: 'Owner',
tags: 'Tag',
domains: 'Domain',
- [DATA_PRODUCTS_FILTER_NAME]: 'Data Product',
platform: 'Platform',
fieldTags: 'Column Tag',
glossaryTerms: 'Glossary Term',
diff --git a/datahub-web-react/src/app/settings/SettingsPage.tsx b/datahub-web-react/src/app/settings/SettingsPage.tsx
index bfec9b395cff2..339cc0cf44bac 100644
--- a/datahub-web-react/src/app/settings/SettingsPage.tsx
+++ b/datahub-web-react/src/app/settings/SettingsPage.tsx
@@ -7,6 +7,7 @@ import {
ToolOutlined,
FilterOutlined,
TeamOutlined,
+ PushpinOutlined,
} from '@ant-design/icons';
import { Redirect, Route, useHistory, useLocation, useRouteMatch, Switch } from 'react-router';
import styled from 'styled-components';
@@ -19,6 +20,7 @@ import { Preferences } from './Preferences';
import { ManageViews } from '../entity/view/ManageViews';
import { useUserContext } from '../context/useUserContext';
import { ManageOwnership } from '../entity/ownership/ManageOwnership';
+import ManagePosts from './posts/ManagePosts';
const PageContainer = styled.div`
display: flex;
@@ -62,6 +64,7 @@ const PATHS = [
{ path: 'preferences', content: },
{ path: 'views', content: },
{ path: 'ownership', content: },
+ { path: 'posts', content: },
];
/**
@@ -91,6 +94,7 @@ export const SettingsPage = () => {
const showUsersGroups = (isIdentityManagementEnabled && me && me?.platformPrivileges?.manageIdentities) || false;
const showViews = isViewsEnabled || false;
const showOwnershipTypes = me && me?.platformPrivileges?.manageOwnershipTypes;
+ const showHomePagePosts = me && me?.platformPrivileges?.manageGlobalAnnouncements;
return (
@@ -143,6 +147,11 @@ export const SettingsPage = () => {
Ownership Types
)}
+ {showHomePagePosts && (
+
+ Home Page Posts
+
+ )}
diff --git a/datahub-web-react/src/app/settings/posts/CreatePostForm.tsx b/datahub-web-react/src/app/settings/posts/CreatePostForm.tsx
new file mode 100644
index 0000000000000..a8d6cfa64c9c1
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/CreatePostForm.tsx
@@ -0,0 +1,91 @@
+import React, { useState } from 'react';
+import { Form, Input, Typography, FormInstance, Radio } from 'antd';
+import styled from 'styled-components';
+import {
+ DESCRIPTION_FIELD_NAME,
+ LINK_FIELD_NAME,
+ LOCATION_FIELD_NAME,
+ TITLE_FIELD_NAME,
+ TYPE_FIELD_NAME,
+} from './constants';
+import { PostContentType } from '../../../types.generated';
+
+const TopFormItem = styled(Form.Item)`
+ margin-bottom: 24px;
+`;
+
+const SubFormItem = styled(Form.Item)`
+ margin-bottom: 0;
+`;
+
+type Props = {
+ setCreateButtonEnabled: (isEnabled: boolean) => void;
+ form: FormInstance;
+};
+
+export default function CreatePostForm({ setCreateButtonEnabled, form }: Props) {
+ const [postType, setPostType] = useState(PostContentType.Text);
+
+ return (
+ }>
+ setPostType(e.target.value)}
+ value={postType}
+ defaultValue={postType}
+ optionType="button"
+ buttonStyle="solid"
+ >
+ Announcement
+ Link
+
+
+
+ Title}>
+ The title for your new post.
+
+
+
+
+ {postType === PostContentType.Text && (
+ Description}>
+ The main content for your new post.
+
+
+
+
+ )}
+ {postType === PostContentType.Link && (
+ <>
+ Link URL}>
+
+ Where users will be directed when they click this post.
+
+
+
+
+
+ Image URL}>
+
+ A URL to an image you want to display on your link post.
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/datahub-web-react/src/app/settings/posts/CreatePostModal.tsx b/datahub-web-react/src/app/settings/posts/CreatePostModal.tsx
new file mode 100644
index 0000000000000..b4851ecb02969
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/CreatePostModal.tsx
@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+import { Button, Form, message, Modal } from 'antd';
+import CreatePostForm from './CreatePostForm';
+import {
+ CREATE_POST_BUTTON_ID,
+ DESCRIPTION_FIELD_NAME,
+ LINK_FIELD_NAME,
+ LOCATION_FIELD_NAME,
+ TYPE_FIELD_NAME,
+ TITLE_FIELD_NAME,
+} from './constants';
+import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
+import { MediaType, PostContentType, PostType } from '../../../types.generated';
+import { useCreatePostMutation } from '../../../graphql/mutations.generated';
+
+type Props = {
+ onClose: () => void;
+ onCreate: (
+ contentType: string,
+ title: string,
+ description: string | undefined,
+ link: string | undefined,
+ location: string | undefined,
+ ) => void;
+};
+
+export default function CreatePostModal({ onClose, onCreate }: Props) {
+ const [createPostMutation] = useCreatePostMutation();
+ const [createButtonEnabled, setCreateButtonEnabled] = useState(false);
+ const [form] = Form.useForm();
+ const onCreatePost = () => {
+ const contentTypeValue = form.getFieldValue(TYPE_FIELD_NAME) ?? PostContentType.Text;
+ const mediaValue =
+ form.getFieldValue(TYPE_FIELD_NAME) && form.getFieldValue(LOCATION_FIELD_NAME)
+ ? {
+ type: MediaType.Image,
+ location: form.getFieldValue(LOCATION_FIELD_NAME) ?? null,
+ }
+ : null;
+ createPostMutation({
+ variables: {
+ input: {
+ postType: PostType.HomePageAnnouncement,
+ content: {
+ contentType: contentTypeValue,
+ title: form.getFieldValue(TITLE_FIELD_NAME),
+ description: form.getFieldValue(DESCRIPTION_FIELD_NAME) ?? null,
+ link: form.getFieldValue(LINK_FIELD_NAME) ?? null,
+ media: mediaValue,
+ },
+ },
+ },
+ })
+ .then(({ errors }) => {
+ if (!errors) {
+ message.success({
+ content: `Created Post!`,
+ duration: 3,
+ });
+ onCreate(
+ form.getFieldValue(TYPE_FIELD_NAME) ?? PostContentType.Text,
+ form.getFieldValue(TITLE_FIELD_NAME),
+ form.getFieldValue(DESCRIPTION_FIELD_NAME),
+ form.getFieldValue(LINK_FIELD_NAME),
+ form.getFieldValue(LOCATION_FIELD_NAME),
+ );
+ form.resetFields();
+ }
+ })
+ .catch((e) => {
+ message.destroy();
+ message.error({ content: 'Failed to create Post! An unknown error occured.', duration: 3 });
+ console.error('Failed to create Post:', e.message);
+ });
+ onClose();
+ };
+
+ // Handle the Enter press
+ useEnterKeyListener({
+ querySelectorToExecuteClick: '#createPostButton',
+ });
+
+ return (
+
+
+
+ >
+ }
+ >
+
+
+ );
+}
diff --git a/datahub-web-react/src/app/settings/posts/ManagePosts.tsx b/datahub-web-react/src/app/settings/posts/ManagePosts.tsx
new file mode 100644
index 0000000000000..e0f694c192c62
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/ManagePosts.tsx
@@ -0,0 +1,40 @@
+import { Typography } from 'antd';
+import React from 'react';
+import styled from 'styled-components/macro';
+import { PostList } from './PostsList';
+
+const PageContainer = styled.div`
+ padding-top: 20px;
+ width: 100%;
+ height: 100%;
+`;
+
+const PageHeaderContainer = styled.div`
+ && {
+ padding-left: 24px;
+ }
+`;
+
+const PageTitle = styled(Typography.Title)`
+ && {
+ margin-bottom: 12px;
+ }
+`;
+
+const ListContainer = styled.div``;
+
+export default function ManagePosts() {
+ return (
+
+
+ Home Page Posts
+
+ View and manage pinned posts that appear to all users on the landing page.
+
+
+
+
+
+
+ );
+}
diff --git a/datahub-web-react/src/app/settings/posts/PostItemMenu.tsx b/datahub-web-react/src/app/settings/posts/PostItemMenu.tsx
new file mode 100644
index 0000000000000..e3fc424a47ef2
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/PostItemMenu.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { DeleteOutlined } from '@ant-design/icons';
+import { Dropdown, Menu, message, Modal } from 'antd';
+import { MenuIcon } from '../../entity/shared/EntityDropdown/EntityDropdown';
+import { useDeletePostMutation } from '../../../graphql/post.generated';
+
+type Props = {
+ urn: string;
+ title: string;
+ onDelete?: () => void;
+};
+
+export default function PostItemMenu({ title, urn, onDelete }: Props) {
+ const [deletePostMutation] = useDeletePostMutation();
+
+ const deletePost = () => {
+ deletePostMutation({
+ variables: {
+ urn,
+ },
+ })
+ .then(({ errors }) => {
+ if (!errors) {
+ message.success('Deleted Post!');
+ onDelete?.();
+ }
+ })
+ .catch(() => {
+ message.destroy();
+ message.error({ content: `Failed to delete Post!: An unknown error occurred.`, duration: 3 });
+ });
+ };
+
+ const onConfirmDelete = () => {
+ Modal.confirm({
+ title: `Delete Post '${title}'`,
+ content: `Are you sure you want to remove this Post?`,
+ onOk() {
+ deletePost();
+ },
+ onCancel() {},
+ okText: 'Yes',
+ maskClosable: true,
+ closable: true,
+ });
+ };
+
+ return (
+
+
+ Delete
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/datahub-web-react/src/app/settings/posts/PostsList.tsx b/datahub-web-react/src/app/settings/posts/PostsList.tsx
new file mode 100644
index 0000000000000..5ae2be1547f9b
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/PostsList.tsx
@@ -0,0 +1,200 @@
+import React, { useEffect, useState } from 'react';
+import { Button, Empty, Pagination, Typography } from 'antd';
+import { useLocation } from 'react-router';
+import styled from 'styled-components';
+import * as QueryString from 'query-string';
+import { PlusOutlined } from '@ant-design/icons';
+import { AlignType } from 'rc-table/lib/interface';
+import CreatePostModal from './CreatePostModal';
+import { PostColumn, PostEntry, PostListMenuColumn } from './PostsListColumns';
+import { useEntityRegistry } from '../../useEntityRegistry';
+import { useListPostsQuery } from '../../../graphql/post.generated';
+import { scrollToTop } from '../../shared/searchUtils';
+import { addToListPostCache, removeFromListPostCache } from './utils';
+import { Message } from '../../shared/Message';
+import TabToolbar from '../../entity/shared/components/styled/TabToolbar';
+import { SearchBar } from '../../search/SearchBar';
+import { StyledTable } from '../../entity/shared/components/styled/StyledTable';
+import { POST_TYPE_TO_DISPLAY_TEXT } from './constants';
+
+const PostsContainer = styled.div``;
+
+export const PostsPaginationContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ padding: 12px;
+ padding-left: 16px;
+ border-bottom: 1px solid;
+ border-color: ${(props) => props.theme.styles['border-color-base']};
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const PaginationInfo = styled(Typography.Text)`
+ padding: 0px;
+`;
+
+const DEFAULT_PAGE_SIZE = 10;
+
+export const PostList = () => {
+ const entityRegistry = useEntityRegistry();
+ const location = useLocation();
+ const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
+ const paramsQuery = (params?.query as string) || undefined;
+ const [query, setQuery] = useState(undefined);
+ useEffect(() => setQuery(paramsQuery), [paramsQuery]);
+
+ const [page, setPage] = useState(1);
+ const [isCreatingPost, setIsCreatingPost] = useState(false);
+
+ const pageSize = DEFAULT_PAGE_SIZE;
+ const start = (page - 1) * pageSize;
+
+ const { loading, error, data, client, refetch } = useListPostsQuery({
+ variables: {
+ input: {
+ start,
+ count: pageSize,
+ query,
+ },
+ },
+ fetchPolicy: query && query.length > 0 ? 'no-cache' : 'cache-first',
+ });
+
+ const totalPosts = data?.listPosts?.total || 0;
+ const lastResultIndex = start + pageSize > totalPosts ? totalPosts : start + pageSize;
+ const posts = data?.listPosts?.posts || [];
+
+ const onChangePage = (newPage: number) => {
+ scrollToTop();
+ setPage(newPage);
+ };
+
+ const handleDelete = (urn: string) => {
+ removeFromListPostCache(client, urn, page, pageSize);
+ setTimeout(() => {
+ refetch?.();
+ }, 2000);
+ };
+
+ const allColumns = [
+ {
+ title: 'Title',
+ dataIndex: '',
+ key: 'title',
+ sorter: (sourceA, sourceB) => {
+ return sourceA.title.localeCompare(sourceB.title);
+ },
+ render: (record: PostEntry) => PostColumn(record.title, 200),
+ width: '20%',
+ },
+ {
+ title: 'Description',
+ dataIndex: '',
+ key: 'description',
+ render: (record: PostEntry) => PostColumn(record.description || ''),
+ },
+ {
+ title: 'Type',
+ dataIndex: '',
+ key: 'type',
+ render: (record: PostEntry) => PostColumn(POST_TYPE_TO_DISPLAY_TEXT[record.contentType]),
+ style: { minWidth: 100 },
+ width: '10%',
+ },
+ {
+ title: '',
+ dataIndex: '',
+ width: '5%',
+ align: 'right' as AlignType,
+ key: 'menu',
+ render: PostListMenuColumn(handleDelete),
+ },
+ ];
+
+ const tableData = posts.map((post) => {
+ return {
+ urn: post.urn,
+ title: post.content.title,
+ description: post.content.description,
+ contentType: post.content.contentType,
+ };
+ });
+
+ return (
+ <>
+ {!data && loading && }
+ {error && }
+
+
+
+ null}
+ onQueryChange={(q) => setQuery(q && q.length > 0 ? q : undefined)}
+ entityRegistry={entityRegistry}
+ hideRecommendations
+ />
+
+ }}
+ />
+ {totalPosts > pageSize && (
+
+
+
+ {lastResultIndex > 0 ? (page - 1) * pageSize + 1 : 0} - {lastResultIndex}
+ {' '}
+ of {totalPosts}
+
+
+
+
+ )}
+ {isCreatingPost && (
+ setIsCreatingPost(false)}
+ onCreate={(urn, title, description) => {
+ addToListPostCache(
+ client,
+ {
+ urn,
+ properties: {
+ title,
+ description: description || null,
+ },
+ },
+ pageSize,
+ );
+ setTimeout(() => refetch(), 2000);
+ }}
+ />
+ )}
+
+ >
+ );
+};
diff --git a/datahub-web-react/src/app/settings/posts/PostsListColumns.tsx b/datahub-web-react/src/app/settings/posts/PostsListColumns.tsx
new file mode 100644
index 0000000000000..38f910baf8f41
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/PostsListColumns.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+// import { Typography } from 'antd';
+import styled from 'styled-components/macro';
+import { Maybe } from 'graphql/jsutils/Maybe';
+import PostItemMenu from './PostItemMenu';
+
+export interface PostEntry {
+ title: string;
+ contentType: string;
+ description: Maybe;
+ urn: string;
+}
+
+const PostText = styled.div<{ minWidth?: number }>`
+ ${(props) => props.minWidth !== undefined && `min-width: ${props.minWidth}px;`}
+`;
+
+export function PostListMenuColumn(handleDelete: (urn: string) => void) {
+ return (record: PostEntry) => (
+ handleDelete(record.urn)} />
+ );
+}
+
+export function PostColumn(text: string, minWidth?: number) {
+ return {text};
+}
diff --git a/datahub-web-react/src/app/settings/posts/constants.ts b/datahub-web-react/src/app/settings/posts/constants.ts
new file mode 100644
index 0000000000000..5a164019fe2e5
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/constants.ts
@@ -0,0 +1,13 @@
+import { PostContentType } from '../../../types.generated';
+
+export const TITLE_FIELD_NAME = 'title';
+export const DESCRIPTION_FIELD_NAME = 'description';
+export const LINK_FIELD_NAME = 'link';
+export const LOCATION_FIELD_NAME = 'location';
+export const TYPE_FIELD_NAME = 'type';
+export const CREATE_POST_BUTTON_ID = 'createPostButton';
+
+export const POST_TYPE_TO_DISPLAY_TEXT = {
+ [PostContentType.Link]: 'Link',
+ [PostContentType.Text]: 'Announcement',
+};
diff --git a/datahub-web-react/src/app/settings/posts/utils.ts b/datahub-web-react/src/app/settings/posts/utils.ts
new file mode 100644
index 0000000000000..ce48c7400738c
--- /dev/null
+++ b/datahub-web-react/src/app/settings/posts/utils.ts
@@ -0,0 +1,77 @@
+import { ListPostsDocument, ListPostsQuery } from '../../../graphql/post.generated';
+
+/**
+ * Add an entry to the list posts cache.
+ */
+export const addToListPostCache = (client, newPost, pageSize) => {
+ // Read the data from our cache for this query.
+ const currData: ListPostsQuery | null = client.readQuery({
+ query: ListPostsDocument,
+ variables: {
+ input: {
+ start: 0,
+ count: pageSize,
+ },
+ },
+ });
+
+ // Add our new post into the existing list.
+ const newPosts = [newPost, ...(currData?.listPosts?.posts || [])];
+
+ // Write our data back to the cache.
+ client.writeQuery({
+ query: ListPostsDocument,
+ variables: {
+ input: {
+ start: 0,
+ count: pageSize,
+ },
+ },
+ data: {
+ listPosts: {
+ start: 0,
+ count: (currData?.listPosts?.count || 0) + 1,
+ total: (currData?.listPosts?.total || 0) + 1,
+ posts: newPosts,
+ },
+ },
+ });
+};
+
+/**
+ * Remove an entry from the list posts cache.
+ */
+export const removeFromListPostCache = (client, urn, page, pageSize) => {
+ // Read the data from our cache for this query.
+ const currData: ListPostsQuery | null = client.readQuery({
+ query: ListPostsDocument,
+ variables: {
+ input: {
+ start: (page - 1) * pageSize,
+ count: pageSize,
+ },
+ },
+ });
+
+ // Remove the post from the existing posts set.
+ const newPosts = [...(currData?.listPosts?.posts || []).filter((post) => post.urn !== urn)];
+
+ // Write our data back to the cache.
+ client.writeQuery({
+ query: ListPostsDocument,
+ variables: {
+ input: {
+ start: (page - 1) * pageSize,
+ count: pageSize,
+ },
+ },
+ data: {
+ listPosts: {
+ start: currData?.listPosts?.start || 0,
+ count: (currData?.listPosts?.count || 1) - 1,
+ total: (currData?.listPosts?.total || 1) - 1,
+ posts: newPosts,
+ },
+ },
+ });
+};
diff --git a/datahub-web-react/src/app/shared/tags/tag/Tag.tsx b/datahub-web-react/src/app/shared/tags/tag/Tag.tsx
index 2288238091776..ed2460b6eea3c 100644
--- a/datahub-web-react/src/app/shared/tags/tag/Tag.tsx
+++ b/datahub-web-react/src/app/shared/tags/tag/Tag.tsx
@@ -8,6 +8,7 @@ import { StyledTag } from '../../../entity/shared/components/styled/StyledTag';
import { HoverEntityTooltip } from '../../../recommendations/renderer/component/HoverEntityTooltip';
import { useEntityRegistry } from '../../../useEntityRegistry';
import { TagProfileDrawer } from '../TagProfileDrawer';
+import { useHasMatchedFieldByUrn } from '../../../search/context/SearchResultContext';
const TagLink = styled.span`
display: inline-block;
@@ -41,6 +42,7 @@ export default function Tag({
}: Props) {
const entityRegistry = useEntityRegistry();
const [removeTagMutation] = useRemoveTagMutation();
+ const highlightTag = useHasMatchedFieldByUrn(tag.tag.urn, 'tags');
const [tagProfileDrawerVisible, setTagProfileDrawerVisible] = useState(false);
const [addTagUrn, setAddTagUrn] = useState('');
@@ -110,6 +112,7 @@ export default function Tag({
removeTag(tag);
}}
fontSize={fontSize}
+ highlightTag={highlightTag}
>
`
+const StyledTag = styled(Tag)<{ fontSize?: number; highlightTerm?: boolean }>`
+ &&& {
+ ${(props) =>
+ props.highlightTerm &&
+ `
+ background: ${props.theme.styles['highlight-color']};
+ border: 1px solid ${props.theme.styles['highlight-border-color']};
+ `}
+ }
${(props) => props.fontSize && `font-size: ${props.fontSize}px;`}
`;
@@ -38,6 +47,7 @@ export default function TermContent({
}: Props) {
const entityRegistry = useEntityRegistry();
const [removeTermMutation] = useRemoveTermMutation();
+ const highlightTerm = useHasMatchedFieldByUrn(term.term.urn, 'glossaryTerms');
const removeTerm = (termToRemove: GlossaryTermAssociation) => {
onOpenModal?.();
@@ -85,6 +95,7 @@ export default function TermContent({
removeTerm(term);
}}
fontSize={fontSize}
+ highlightTerm={highlightTerm}
>
diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx
index 3b34b108ecc93..807a17c4fd6a4 100644
--- a/datahub-web-react/src/appConfigContext.tsx
+++ b/datahub-web-react/src/appConfigContext.tsx
@@ -27,6 +27,9 @@ export const DEFAULT_APP_CONFIG = {
entityProfile: {
domainDefaultTab: null,
},
+ searchResult: {
+ enableNameHighlight: false,
+ },
},
authConfig: {
tokenAuthEnabled: false,
diff --git a/datahub-web-react/src/conf/Global.ts b/datahub-web-react/src/conf/Global.ts
index b16dd1eaace57..e1220b8c81b53 100644
--- a/datahub-web-react/src/conf/Global.ts
+++ b/datahub-web-react/src/conf/Global.ts
@@ -28,6 +28,7 @@ export enum PageRoutes {
SETTINGS_VIEWS = '/settings/views',
EMBED = '/embed',
EMBED_LOOKUP = '/embed/lookup/:url',
+ SETTINGS_POSTS = '/settings/posts',
}
/**
diff --git a/datahub-web-react/src/conf/theme/theme_dark.config.json b/datahub-web-react/src/conf/theme/theme_dark.config.json
index b648f3d997f21..9746c3ddde5f3 100644
--- a/datahub-web-react/src/conf/theme/theme_dark.config.json
+++ b/datahub-web-react/src/conf/theme/theme_dark.config.json
@@ -17,7 +17,9 @@
"disabled-color": "fade(white, 25%)",
"steps-nav-arrow-color": "fade(white, 25%)",
"homepage-background-upper-fade": "#FFFFFF",
- "homepage-background-lower-fade": "#333E4C"
+ "homepage-background-lower-fade": "#333E4C",
+ "highlight-color": "#E6F4FF",
+ "highlight-border-color": "#BAE0FF"
},
"assets": {
"logoUrl": "/assets/logo.png"
diff --git a/datahub-web-react/src/conf/theme/theme_light.config.json b/datahub-web-react/src/conf/theme/theme_light.config.json
index e842fdb1bb8aa..906c04e38a1ba 100644
--- a/datahub-web-react/src/conf/theme/theme_light.config.json
+++ b/datahub-web-react/src/conf/theme/theme_light.config.json
@@ -20,7 +20,9 @@
"homepage-background-lower-fade": "#FFFFFF",
"homepage-text-color": "#434343",
"box-shadow": "0px 0px 30px 0px rgb(239 239 239)",
- "box-shadow-hover": "0px 1px 0px 0.5px rgb(239 239 239)"
+ "box-shadow-hover": "0px 1px 0px 0.5px rgb(239 239 239)",
+ "highlight-color": "#E6F4FF",
+ "highlight-border-color": "#BAE0FF"
},
"assets": {
"logoUrl": "/assets/logo.png"
diff --git a/datahub-web-react/src/conf/theme/types.ts b/datahub-web-react/src/conf/theme/types.ts
index 98140cbbd553d..7d78230092700 100644
--- a/datahub-web-react/src/conf/theme/types.ts
+++ b/datahub-web-react/src/conf/theme/types.ts
@@ -18,6 +18,8 @@ export type Theme = {
'homepage-background-lower-fade': string;
'box-shadow': string;
'box-shadow-hover': string;
+ 'highlight-color': string;
+ 'highlight-border-color': string;
};
assets: {
logoUrl: string;
diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql
index 4b1295f1024a2..bf15e5f757f8f 100644
--- a/datahub-web-react/src/graphql/app.graphql
+++ b/datahub-web-react/src/graphql/app.graphql
@@ -45,6 +45,9 @@ query appConfig {
defaultTab
}
}
+ searchResult {
+ enableNameHighlight
+ }
}
telemetryConfig {
enableThirdPartyLogging
diff --git a/datahub-web-react/src/graphql/me.graphql b/datahub-web-react/src/graphql/me.graphql
index 2c693c747af56..af850c9c3ce28 100644
--- a/datahub-web-react/src/graphql/me.graphql
+++ b/datahub-web-react/src/graphql/me.graphql
@@ -46,6 +46,7 @@ query getMe {
createTags
manageGlobalViews
manageOwnershipTypes
+ manageGlobalAnnouncements
}
}
}
diff --git a/datahub-web-react/src/graphql/post.graphql b/datahub-web-react/src/graphql/post.graphql
index c19f38fc7751c..ee092ad4fba90 100644
--- a/datahub-web-react/src/graphql/post.graphql
+++ b/datahub-web-react/src/graphql/post.graphql
@@ -20,3 +20,11 @@ query listPosts($input: ListPostsInput!) {
}
}
}
+
+mutation createPost($input: CreatePostInput!) {
+ createPost(input: $input)
+}
+
+mutation deletePost($urn: String!) {
+ deletePost(urn: $urn)
+}
diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql
index 09610c9a9cfc1..7cd868d7cd2b2 100644
--- a/datahub-web-react/src/graphql/search.graphql
+++ b/datahub-web-react/src/graphql/search.graphql
@@ -2,6 +2,7 @@ fragment autoCompleteFields on Entity {
urn
type
... on Dataset {
+ exists
name
platform {
...platformFields
@@ -19,6 +20,29 @@ fragment autoCompleteFields on Entity {
subTypes {
typeNames
}
+ siblings {
+ isPrimary
+ siblings {
+ urn
+ type
+ ... on Dataset {
+ exists
+ platform {
+ ...platformFields
+ }
+ parentContainers {
+ ...parentContainersFields
+ }
+ name
+ properties {
+ name
+ description
+ qualifiedName
+ externalUrl
+ }
+ }
+ }
+ }
...datasetStatsFields
}
... on CorpUser {
@@ -808,6 +832,11 @@ fragment searchResults on SearchResults {
matchedFields {
name
value
+ entity {
+ urn
+ type
+ ...entityDisplayNameFields
+ }
}
insights {
text
@@ -817,6 +846,11 @@ fragment searchResults on SearchResults {
facets {
...facetFields
}
+ suggestions {
+ text
+ frequency
+ score
+ }
}
fragment schemaFieldEntityFields on SchemaFieldEntity {
diff --git a/docker/build.gradle b/docker/build.gradle
index f33e06f383240..ae101fe1defc5 100644
--- a/docker/build.gradle
+++ b/docker/build.gradle
@@ -35,8 +35,31 @@ task quickstart(type: Exec, dependsOn: ':metadata-ingestion:install') {
environment "DATAHUB_TELEMETRY_ENABLED", "false"
environment "DOCKER_COMPOSE_BASE", "file://${rootProject.projectDir}"
- environment "ACTIONS_VERSION", 'alpine3.17-slim'
- environment "DATAHUB_ACTIONS_IMAGE", 'nginx'
+ // environment "ACTIONS_VERSION", 'alpine3.17-slim'
+ // environment "DATAHUB_ACTIONS_IMAGE", 'nginx'
+
+ def cmd = [
+ 'source ../metadata-ingestion/venv/bin/activate && ',
+ 'datahub docker quickstart',
+ '--no-pull-images',
+ '--standalone_consumers',
+ '--version', "v${version}",
+ '--dump-logs-on-failure'
+ ]
+
+ commandLine 'bash', '-c', cmd.join(" ")
+}
+
+task quickstartSlim(type: Exec, dependsOn: ':metadata-ingestion:install') {
+ dependsOn(([':docker:datahub-ingestion'] + quickstart_modules).collect { it + ':dockerTag' })
+ shouldRunAfter ':metadata-ingestion:clean', 'quickstartNuke'
+
+ environment "DATAHUB_TELEMETRY_ENABLED", "false"
+ environment "DOCKER_COMPOSE_BASE", "file://${rootProject.projectDir}"
+ environment "DATAHUB_ACTIONS_IMAGE", "acryldata/datahub-ingestion"
+ environment "ACTIONS_VERSION", "v${version}-slim"
+ environment "ACTIONS_EXTRA_PACKAGES", 'acryl-datahub-actions[executor] acryl-datahub-actions'
+ environment "ACTIONS_CONFIG", 'https://raw.githubusercontent.com/acryldata/datahub-actions/main/docker/config/executor.yaml'
def cmd = [
'source ../metadata-ingestion/venv/bin/activate && ',
@@ -64,6 +87,7 @@ task quickstartDebug(type: Exec, dependsOn: ':metadata-ingestion:install') {
dependsOn(debug_modules.collect { it + ':dockerTagDebug' })
shouldRunAfter ':metadata-ingestion:clean', 'quickstartNuke'
+ environment "DATAHUB_PRECREATE_TOPICS", "true"
environment "DATAHUB_TELEMETRY_ENABLED", "false"
environment "DOCKER_COMPOSE_BASE", "file://${rootProject.projectDir}"
diff --git a/docker/datahub-ingestion-base/Dockerfile b/docker/datahub-ingestion-base/Dockerfile
index 9893d44caf460..3d47f79617370 100644
--- a/docker/datahub-ingestion-base/Dockerfile
+++ b/docker/datahub-ingestion-base/Dockerfile
@@ -1,3 +1,6 @@
+ARG APP_ENV=full
+ARG BASE_IMAGE=base
+
FROM golang:1-alpine3.17 AS binary
ENV DOCKERIZE_VERSION v0.6.1
@@ -16,9 +19,7 @@ ENV CONFLUENT_KAFKA_VERSION=1.6.1
ENV DEBIAN_FRONTEND noninteractive
-RUN apt-get update && apt-get install -y \
- && apt-get install -y -qq \
- # gcc \
+RUN apt-get update && apt-get install -y -qq \
make \
python3-ldap \
libldap2-dev \
@@ -31,15 +32,34 @@ RUN apt-get update && apt-get install -y \
zip \
unzip \
ldap-utils \
- openjdk-11-jre-headless \
- && python -m pip install --upgrade pip wheel setuptools==57.5.0 \
- && curl -Lk -o /root/librdkafka-${LIBRDKAFKA_VERSION}.tar.gz https://github.com/edenhill/librdkafka/archive/v${LIBRDKAFKA_VERSION}.tar.gz \
- && tar -xzf /root/librdkafka-${LIBRDKAFKA_VERSION}.tar.gz -C /root \
- && cd /root/librdkafka-${LIBRDKAFKA_VERSION} \
- && ./configure --prefix /usr && make && make install && make clean && ./configure --clean \
- && apt-get remove -y make
+ && python -m pip install --no-cache --upgrade pip wheel setuptools \
+ && wget -q https://github.com/edenhill/librdkafka/archive/v${LIBRDKAFKA_VERSION}.tar.gz -O - | \
+ tar -xz -C /root \
+ && cd /root/librdkafka-${LIBRDKAFKA_VERSION} \
+ && ./configure --prefix /usr && make && make install && cd .. && rm -rf /root/librdkafka-${LIBRDKAFKA_VERSION} \
+ && apt-get remove -y make \
+ && rm -rf /var/lib/apt/lists/* /var/cache/apk/*
+
+# compiled against newer golang for security fixes
COPY --from=binary /go/bin/dockerize /usr/local/bin
+COPY ./docker/datahub-ingestion-base/base-requirements.txt requirements.txt
+COPY ./docker/datahub-ingestion-base/entrypoint.sh /entrypoint.sh
+
+RUN pip install --no-cache -r requirements.txt && \
+ pip uninstall -y acryl-datahub && \
+ chmod +x /entrypoint.sh && \
+ addgroup --gid 1000 datahub && \
+ adduser --disabled-password --uid 1000 --gid 1000 --home /datahub-ingestion datahub
+
+ENTRYPOINT [ "/entrypoint.sh" ]
+
+FROM ${BASE_IMAGE} as full-install
+
+RUN apt-get update && apt-get install -y -qq \
+ default-jre-headless \
+ && rm -rf /var/lib/apt/lists/* /var/cache/apk/*
+
RUN if [ $(arch) = "x86_64" ]; then \
mkdir /opt/oracle && \
cd /opt/oracle && \
@@ -58,7 +78,10 @@ RUN if [ $(arch) = "x86_64" ]; then \
ldconfig; \
fi;
-COPY ./docker/datahub-ingestion-base/base-requirements.txt requirements.txt
+FROM ${BASE_IMAGE} as slim-install
+# Do nothing else on top of base
+
+FROM ${APP_ENV}-install
-RUN pip install -r requirements.txt && \
- pip uninstall -y acryl-datahub
+USER datahub
+ENV PATH="/datahub-ingestion/.local/bin:$PATH"
\ No newline at end of file
diff --git a/docker/datahub-ingestion-base/base-requirements.txt b/docker/datahub-ingestion-base/base-requirements.txt
index 3d9e0777e5ce0..82d9a93a9a2c3 100644
--- a/docker/datahub-ingestion-base/base-requirements.txt
+++ b/docker/datahub-ingestion-base/base-requirements.txt
@@ -1,3 +1,7 @@
+# Excluded for slim
+# pyspark==3.0.3
+# pydeequ==1.0.1
+
acryl-datahub-classify==0.0.6
acryl-iceberg-legacy==0.0.4
acryl-PyHive==0.6.13
@@ -253,7 +257,6 @@ pycryptodome==3.18.0
pycryptodomex==3.18.0
pydantic==1.10.8
pydash==7.0.3
-pydeequ==1.0.1
pydruid==0.6.5
Pygments==2.15.1
pymongo==4.3.3
@@ -261,7 +264,6 @@ PyMySQL==1.0.3
pyOpenSSL==22.0.0
pyparsing==3.0.9
pyrsistent==0.19.3
-pyspark==3.0.3
pyspnego==0.9.0
python-daemon==3.0.1
python-dateutil==2.8.2
diff --git a/docker/datahub-ingestion-base/build.gradle b/docker/datahub-ingestion-base/build.gradle
index fe3c12a59886f..10cd2ee71cce3 100644
--- a/docker/datahub-ingestion-base/build.gradle
+++ b/docker/datahub-ingestion-base/build.gradle
@@ -12,14 +12,17 @@ ext {
}
docker {
- name "${docker_registry}/${docker_repo}:v${version}"
- version "v${version}"
+ name "${docker_registry}/${docker_repo}:v${version}-slim"
+ version "v${version}-slim"
dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
+ buildArgs([APP_ENV: 'slim'])
}
-tasks.getByPath('docker').dependsOn('build')
+tasks.getByName('docker').dependsOn('build')
task mkdirBuildDocker {
doFirst {
@@ -27,10 +30,11 @@ task mkdirBuildDocker {
}
}
dockerClean.finalizedBy(mkdirBuildDocker)
+dockerClean.dependsOn([':docker:datahub-ingestion:dockerClean'])
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/docker/datahub-ingestion-base/entrypoint.sh b/docker/datahub-ingestion-base/entrypoint.sh
new file mode 100644
index 0000000000000..518bb21561467
--- /dev/null
+++ b/docker/datahub-ingestion-base/entrypoint.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/bash
+
+if [ ! -z "$ACTIONS_EXTRA_PACKAGES" ]; then
+ pip install --user $ACTIONS_EXTRA_PACKAGES
+fi
+
+if [[ ! -z "$ACTIONS_CONFIG" && ! -z "$ACTIONS_EXTRA_PACKAGES" ]]; then
+ mkdir -p /tmp/datahub/logs
+ curl -q "$ACTIONS_CONFIG" -o config.yaml
+ exec dockerize -wait ${DATAHUB_GMS_PROTOCOL:-http}://$DATAHUB_GMS_HOST:$DATAHUB_GMS_PORT/health -timeout 240s \
+ datahub actions --config config.yaml
+else
+ exec datahub $@
+fi
diff --git a/docker/datahub-ingestion-slim/Dockerfile b/docker/datahub-ingestion-slim/Dockerfile
deleted file mode 100644
index 580dcc4277124..0000000000000
--- a/docker/datahub-ingestion-slim/Dockerfile
+++ /dev/null
@@ -1,9 +0,0 @@
-# Defining environment
-ARG APP_ENV=prod
-ARG DOCKER_VERSION=latest
-
-FROM acryldata/datahub-ingestion:$DOCKER_VERSION as base
-
-USER 0
-RUN pip uninstall -y pyspark
-USER datahub
diff --git a/docker/datahub-ingestion-slim/build.gradle b/docker/datahub-ingestion-slim/build.gradle
deleted file mode 100644
index f21b66b576a0c..0000000000000
--- a/docker/datahub-ingestion-slim/build.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-plugins {
- id 'com.palantir.docker'
- id 'java' // required for versioning
-}
-
-apply from: "../../gradle/versioning/versioning.gradle"
-
-ext {
- docker_registry = rootProject.ext.docker_registry == 'linkedin' ? 'acryldata' : docker_registry
- docker_repo = 'datahub-ingestion-slim'
- docker_dir = 'datahub-ingestion-slim'
-}
-
-docker {
- name "${docker_registry}/${docker_repo}:v${version}"
- version "v${version}"
- dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
- files fileTree(rootProject.projectDir) {
- include "docker/${docker_dir}/*"
- }
- buildArgs([DOCKER_VERSION: version])
-
- buildx(false)
-}
-tasks.getByPath('docker').dependsOn(['build', ':docker:datahub-ingestion:docker'])
-
-task mkdirBuildDocker {
- doFirst {
- mkdir "${project.buildDir}/docker"
- }
-}
-dockerClean.finalizedBy(mkdirBuildDocker)
-
-task cleanLocalDockerImages {
- doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
- }
-}
-dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/docker/datahub-ingestion/Dockerfile b/docker/datahub-ingestion/Dockerfile
index 45a98efb7f6fb..0ecc30d02ac3f 100644
--- a/docker/datahub-ingestion/Dockerfile
+++ b/docker/datahub-ingestion/Dockerfile
@@ -1,42 +1,27 @@
# Defining environment
-ARG APP_ENV=prod
+ARG APP_ENV=full
+ARG BASE_IMAGE=acryldata/datahub-ingestion-base
ARG DOCKER_VERSION=latest
-FROM acryldata/datahub-ingestion-base:$DOCKER_VERSION as base
-
-FROM eclipse-temurin:11 as prod-build
-COPY . /datahub-src
-WORKDIR /datahub-src
-# We noticed that the gradle wrapper download failed frequently on in CI on arm64 machines.
-# I suspect this was due because of the QEMU emulation slowdown, combined with the arm64
-# build being starved for CPU by the x86_64 build's codegen step.
-#
-# The middle step will attempt to download gradle wrapper 5 times with exponential backoff.
-# The ./gradlew --version will force the download of the gradle wrapper but is otherwise a no-op.
-# Note that the retry logic will always return success, so we should always attempt to run codegen.
-# Inspired by https://github.com/gradle/gradle/issues/18124#issuecomment-958182335.
-# and https://unix.stackexchange.com/a/82610/378179.
-# This is a workaround for https://github.com/gradle/gradle/issues/18124.
-RUN (for attempt in 1 2 3 4 5; do ./gradlew --version && break ; echo "Failed to download gradle wrapper (attempt $attempt)" && sleep $((2<<$attempt)) ; done ) && \
- ./gradlew :metadata-events:mxe-schemas:build
-
-FROM base as prod-codegen
-COPY --from=prod-build /datahub-src /datahub-src
-RUN cd /datahub-src/metadata-ingestion && \
- pip install -e ".[base]" && \
- ./scripts/codegen.sh
-
-FROM base as prod-install
-COPY --from=prod-codegen /datahub-src/metadata-ingestion /datahub-ingestion
-COPY --from=prod-codegen /root/.cache/pip /root/.cache/pip
+FROM $BASE_IMAGE:$DOCKER_VERSION as base
+USER 0
+
+COPY ./metadata-ingestion /datahub-ingestion
+
ARG RELEASE_VERSION
-RUN cd /datahub-ingestion && \
- sed -i.bak "s/__version__ = \"0.0.0.dev0\"/__version__ = \"$RELEASE_VERSION\"/" src/datahub/__init__.py && \
+WORKDIR /datahub-ingestion
+RUN sed -i.bak "s/__version__ = \"0.0.0.dev0\"/__version__ = \"$RELEASE_VERSION\"/" src/datahub/__init__.py && \
cat src/datahub/__init__.py && \
- pip install ".[all]" && \
- pip freeze && \
- # This is required to fix security vulnerability in htrace-core4
- rm -f /usr/local/lib/python3.10/site-packages/pyspark/jars/htrace-core4-4.1.0-incubating.jar
+ chown -R datahub /datahub-ingestion
+
+USER datahub
+ENV PATH="/datahub-ingestion/.local/bin:$PATH"
+
+FROM base as slim-install
+RUN pip install --no-cache --user ".[base,datahub-rest,datahub-kafka,snowflake,bigquery,redshift,mysql,postgres,hive,clickhouse,glue,dbt,looker,lookml,tableau,powerbi,superset,datahub-business-glossary]"
+
+FROM base as full-install
+RUN pip install --no-cache --user ".[all]"
FROM base as dev-install
# Dummy stage for development. Assumes code is built on your machine and mounted to this image.
@@ -44,7 +29,5 @@ FROM base as dev-install
FROM ${APP_ENV}-install as final
-RUN addgroup --system datahub && adduser --system datahub --ingroup datahub
USER datahub
-
-ENTRYPOINT [ "datahub" ]
+ENV PATH="/datahub-ingestion/.local/bin:$PATH"
diff --git a/docker/datahub-ingestion/build.gradle b/docker/datahub-ingestion/build.gradle
index 7a24d87794c0e..22531c0c4fd0e 100644
--- a/docker/datahub-ingestion/build.gradle
+++ b/docker/datahub-ingestion/build.gradle
@@ -11,24 +11,30 @@ ext {
docker_dir = 'datahub-ingestion'
}
+dependencies {
+ project(':docker:datahub-ingestion-base')
+ project(':metadata-ingestion')
+}
+
docker {
- name "${docker_registry}/${docker_repo}:v${version}"
- version "v${version}"
+ name "${docker_registry}/${docker_repo}:v${version}-slim"
+ version "v${version}-slim"
dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
include "metadata-ingestion/**"
- include "metadata-events/**"
- include "metadata-models/**"
- include "li-utils/**"
- include "docs/**"
- include "gradle/**"
- include "buildSrc/**"
- include "*"
+ }.exclude {
+ i -> i.file.isHidden() ||
+ i.file == buildDir ||
+ i.file == project(':metadata-ingestion').buildDir
}
- buildArgs([DOCKER_VERSION: version])
+ buildArgs([DOCKER_VERSION: version,
+ RELEASE_VERSION: version.replace('-SNAPSHOT', '').replace('v', '').replace('-slim', ''),
+ APP_ENV: 'slim'])
}
-tasks.getByPath('docker').dependsOn(['build', ':docker:datahub-ingestion-base:docker'])
+tasks.getByName('docker').dependsOn(['build',
+ ':docker:datahub-ingestion-base:docker',
+ ':metadata-ingestion:codegen'])
task mkdirBuildDocker {
doFirst {
@@ -39,7 +45,7 @@ dockerClean.finalizedBy(mkdirBuildDocker)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/docker/docker-compose-with-cassandra.yml b/docker/docker-compose-with-cassandra.yml
index 5ea364dd31ca7..08f8cc1ec9c45 100644
--- a/docker/docker-compose-with-cassandra.yml
+++ b/docker/docker-compose-with-cassandra.yml
@@ -26,6 +26,9 @@ services:
hostname: actions
image: ${DATAHUB_ACTIONS_IMAGE:-acryldata/datahub-actions}:${ACTIONS_VERSION:-head}
env_file: datahub-actions/env/docker.env
+ environment:
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
depends_on:
datahub-gms:
condition: service_healthy
diff --git a/docker/docker-compose-without-neo4j.yml b/docker/docker-compose-without-neo4j.yml
index 10b3f3c0eca5e..a755eda21cbf5 100644
--- a/docker/docker-compose-without-neo4j.yml
+++ b/docker/docker-compose-without-neo4j.yml
@@ -27,6 +27,9 @@ services:
hostname: actions
image: ${DATAHUB_ACTIONS_IMAGE:-acryldata/datahub-actions}:${ACTIONS_VERSION:-head}
env_file: datahub-actions/env/docker.env
+ environment:
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
depends_on:
datahub-gms:
condition: service_healthy
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 9228c11446ddf..d07ea5fa88f8b 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -26,6 +26,9 @@ services:
hostname: actions
image: ${DATAHUB_ACTIONS_IMAGE:-acryldata/datahub-actions}:${ACTIONS_VERSION:-head}
env_file: datahub-actions/env/docker.env
+ environment:
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
depends_on:
datahub-gms:
condition: service_healthy
diff --git a/docker/elasticsearch-setup/build.gradle b/docker/elasticsearch-setup/build.gradle
index cc2fe1ec5c4db..ffee3b9c65cf4 100644
--- a/docker/elasticsearch-setup/build.gradle
+++ b/docker/elasticsearch-setup/build.gradle
@@ -17,6 +17,8 @@ docker {
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
include "metadata-service/restli-servlet-impl/src/main/resources/index/**"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -25,7 +27,7 @@ docker {
load(true)
push(false)
}
-tasks.getByPath('docker').dependsOn('build')
+tasks.getByName('docker').dependsOn('build')
task mkdirBuildDocker {
doFirst {
@@ -36,7 +38,7 @@ dockerClean.finalizedBy(mkdirBuildDocker)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/docker/kafka-setup/Dockerfile b/docker/kafka-setup/Dockerfile
index 8cf9d0869dc9b..5707234b85f57 100644
--- a/docker/kafka-setup/Dockerfile
+++ b/docker/kafka-setup/Dockerfile
@@ -1,5 +1,7 @@
+ARG KAFKA_DOCKER_VERSION=7.4.1
+
# Using as a base image because to get the needed jars for confluent utils
-FROM confluentinc/cp-base-new@sha256:ac4e0f9bcaecdab728740529f37452231fa40760fcf561759fc3b219f46d2cc9 as confluent_base
+FROM confluentinc/cp-base-new:$KAFKA_DOCKER_VERSION as confluent_base
ARG MAVEN_REPO="https://repo1.maven.org/maven2"
ARG SNAKEYAML_VERSION="2.0"
@@ -16,12 +18,6 @@ ENV SCALA_VERSION 2.13
# Set the classpath for JARs required by `cub`
ENV CUB_CLASSPATH='"/usr/share/java/cp-base-new/*"'
-# Confluent Docker Utils Version (Namely the tag or branch to grab from git to install)
-ARG PYTHON_CONFLUENT_DOCKER_UTILS_VERSION="v0.0.60"
-
-# This can be overriden for an offline/air-gapped builds
-ARG PYTHON_CONFLUENT_DOCKER_UTILS_INSTALL_SPEC="git+https://github.com/confluentinc/confluent-docker-utils@${PYTHON_CONFLUENT_DOCKER_UTILS_VERSION}"
-
LABEL name="kafka" version=${KAFKA_VERSION}
RUN apk add --no-cache bash coreutils
@@ -39,7 +35,6 @@ RUN mkdir -p /opt \
&& pip install --no-cache-dir --upgrade pip wheel setuptools \
&& pip install jinja2 requests \
&& pip install "Cython<3.0" "PyYAML<6" --no-build-isolation \
- && pip install --prefer-binary --prefix=/usr/local --upgrade "${PYTHON_CONFLUENT_DOCKER_UTILS_INSTALL_SPEC}" \
&& rm -rf /tmp/* \
&& apk del --purge .build-deps
@@ -69,7 +64,8 @@ ENV USE_CONFLUENT_SCHEMA_REGISTRY="TRUE"
COPY docker/kafka-setup/kafka-setup.sh ./kafka-setup.sh
COPY docker/kafka-setup/kafka-config.sh ./kafka-config.sh
COPY docker/kafka-setup/kafka-topic-workers.sh ./kafka-topic-workers.sh
+COPY docker/kafka-setup/kafka-ready.sh ./kafka-ready.sh
-RUN chmod +x ./kafka-setup.sh && chmod +x ./kafka-topic-workers.sh
+RUN chmod +x ./kafka-setup.sh ./kafka-topic-workers.sh ./kafka-ready.sh
CMD ./kafka-setup.sh
diff --git a/docker/kafka-setup/build.gradle b/docker/kafka-setup/build.gradle
index a5d33457e45f7..573ef21c88bf9 100644
--- a/docker/kafka-setup/build.gradle
+++ b/docker/kafka-setup/build.gradle
@@ -16,6 +16,8 @@ docker {
dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -24,7 +26,7 @@ docker {
load(true)
push(false)
}
-tasks.getByPath('docker').dependsOn('build')
+tasks.getByName('docker').dependsOn('build')
task mkdirBuildDocker {
doFirst {
@@ -35,7 +37,7 @@ dockerClean.finalizedBy(mkdirBuildDocker)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
diff --git a/docker/kafka-setup/kafka-ready.sh b/docker/kafka-setup/kafka-ready.sh
new file mode 100755
index 0000000000000..ba87bde047ef5
--- /dev/null
+++ b/docker/kafka-setup/kafka-ready.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+for i in {1..60}
+do
+ kafka-broker-api-versions.sh --command-config $CONNECTION_PROPERTIES_PATH --bootstrap-server $KAFKA_BOOTSTRAP_SERVER
+ if [ $? -eq 0 ]; then
+ break
+ fi
+ if [ $i -eq 60 ]; then
+ echo "Kafka bootstrap server $KAFKA_BOOTSTRAP_SERVER not ready."
+ exit 1
+ fi
+ sleep 5s
+done
diff --git a/docker/kafka-setup/kafka-setup.sh b/docker/kafka-setup/kafka-setup.sh
old mode 100644
new mode 100755
index 7b015421b7963..629e9bc9484ee
--- a/docker/kafka-setup/kafka-setup.sh
+++ b/docker/kafka-setup/kafka-setup.sh
@@ -49,8 +49,8 @@ if [[ -n "$KAFKA_PROPERTIES_SASL_CLIENT_CALLBACK_HANDLER_CLASS" ]]; then
echo "sasl.client.callback.handler.class=$KAFKA_PROPERTIES_SASL_CLIENT_CALLBACK_HANDLER_CLASS" >> $CONNECTION_PROPERTIES_PATH
fi
-cub kafka-ready -c $CONNECTION_PROPERTIES_PATH -b $KAFKA_BOOTSTRAP_SERVER 1 180
-
+# cub kafka-ready -c $CONNECTION_PROPERTIES_PATH -b $KAFKA_BOOTSTRAP_SERVER 1 180
+. kafka-ready.sh
############################################################
# Start Topic Creation Logic
diff --git a/docker/mysql-setup/build.gradle b/docker/mysql-setup/build.gradle
index 48a28f15a581d..0d8941cce4833 100644
--- a/docker/mysql-setup/build.gradle
+++ b/docker/mysql-setup/build.gradle
@@ -17,6 +17,8 @@ docker {
dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -25,7 +27,7 @@ docker {
load(true)
push(false)
}
-tasks.getByPath('docker').dependsOn('build')
+tasks.getByName('docker').dependsOn('build')
task mkdirBuildDocker {
doFirst {
@@ -36,7 +38,7 @@ dockerClean.finalizedBy(mkdirBuildDocker)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}")
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
diff --git a/docker/postgres-setup/build.gradle b/docker/postgres-setup/build.gradle
index a5b0413ec4be8..8a026be09d2b4 100644
--- a/docker/postgres-setup/build.gradle
+++ b/docker/postgres-setup/build.gradle
@@ -17,6 +17,8 @@ docker {
dockerfile file("${rootProject.projectDir}/docker/${docker_dir}/Dockerfile")
files fileTree(rootProject.projectDir) {
include "docker/${docker_dir}/*"
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -25,7 +27,7 @@ docker {
load(true)
push(false)
}
-tasks.getByPath('docker').dependsOn('build')
+tasks.getByName('docker').dependsOn('build')
task mkdirBuildDocker {
doFirst {
@@ -36,7 +38,7 @@ dockerClean.finalizedBy(mkdirBuildDocker)
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}")
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
diff --git a/docker/quickstart/docker-compose-m1.quickstart.yml b/docker/quickstart/docker-compose-m1.quickstart.yml
index 5a8edd6eacf19..38418bc8c41b9 100644
--- a/docker/quickstart/docker-compose-m1.quickstart.yml
+++ b/docker/quickstart/docker-compose-m1.quickstart.yml
@@ -34,6 +34,8 @@ services:
datahub-gms:
condition: service_healthy
environment:
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
- DATAHUB_GMS_HOST=datahub-gms
- DATAHUB_GMS_PORT=8080
- DATAHUB_GMS_PROTOCOL=http
diff --git a/docker/quickstart/docker-compose-without-neo4j-m1.quickstart.yml b/docker/quickstart/docker-compose-without-neo4j-m1.quickstart.yml
index 6d51f2efcfcf2..cf879faa6a3f0 100644
--- a/docker/quickstart/docker-compose-without-neo4j-m1.quickstart.yml
+++ b/docker/quickstart/docker-compose-without-neo4j-m1.quickstart.yml
@@ -34,6 +34,8 @@ services:
datahub-gms:
condition: service_healthy
environment:
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
- DATAHUB_GMS_HOST=datahub-gms
- DATAHUB_GMS_PORT=8080
- DATAHUB_GMS_PROTOCOL=http
diff --git a/docker/quickstart/docker-compose-without-neo4j.quickstart.yml b/docker/quickstart/docker-compose-without-neo4j.quickstart.yml
index 48f2d797bd8a4..007830078d2b4 100644
--- a/docker/quickstart/docker-compose-without-neo4j.quickstart.yml
+++ b/docker/quickstart/docker-compose-without-neo4j.quickstart.yml
@@ -34,6 +34,8 @@ services:
datahub-gms:
condition: service_healthy
environment:
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
- DATAHUB_GMS_HOST=datahub-gms
- DATAHUB_GMS_PORT=8080
- DATAHUB_GMS_PROTOCOL=http
diff --git a/docker/quickstart/docker-compose.quickstart.yml b/docker/quickstart/docker-compose.quickstart.yml
index bd30c359a2a76..390543b92123f 100644
--- a/docker/quickstart/docker-compose.quickstart.yml
+++ b/docker/quickstart/docker-compose.quickstart.yml
@@ -34,6 +34,8 @@ services:
datahub-gms:
condition: service_healthy
environment:
+ - ACTIONS_CONFIG=${ACTIONS_CONFIG:-}
+ - ACTIONS_EXTRA_PACKAGES=${ACTIONS_EXTRA_PACKAGES:-}
- DATAHUB_GMS_HOST=datahub-gms
- DATAHUB_GMS_PORT=8080
- DATAHUB_GMS_PROTOCOL=http
diff --git a/docs/advanced/no-code-modeling.md b/docs/advanced/no-code-modeling.md
index e1fadee6d371a..9c8f6761a62bc 100644
--- a/docs/advanced/no-code-modeling.md
+++ b/docs/advanced/no-code-modeling.md
@@ -211,7 +211,7 @@ record ServiceKey {
* Name of the service
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true
}
name: string
diff --git a/docs/authorization/access-policies-guide.md b/docs/authorization/access-policies-guide.md
index 5820e513a83e3..1eabb64d2878f 100644
--- a/docs/authorization/access-policies-guide.md
+++ b/docs/authorization/access-policies-guide.md
@@ -110,10 +110,13 @@ In the second step, we can simply select the Privileges that this Platform Polic
| Manage Tags | Allow the actor to create and remove any Tags |
| Manage Public Views | Allow the actor to create, edit, and remove any public (shared) Views. |
| Manage Ownership Types | Allow the actor to create, edit, and remove any Ownership Types. |
+| Manage Platform Settings | (Acryl DataHub only) Allow the actor to manage global integrations and notification settings |
+| Manage Monitors | (Acryl DataHub only) Allow the actor to create, remove, start, or stop any entity assertion monitors |
| Restore Indices API[^1] | Allow the actor to restore indices for a set of entities via API |
| Enable/Disable Writeability API[^1] | Allow the actor to enable or disable GMS writeability for use in data migrations |
| Apply Retention API[^1] | Allow the actor to apply aspect retention via API |
+
[^1]: Only active if REST_API_AUTHORIZATION_ENABLED environment flag is enabled
#### Step 3: Choose Policy Actors
@@ -204,8 +207,15 @@ The common Metadata Privileges, which span across entity types, include:
| Edit Status | Allow actor to edit the status of an entity (soft deleted or not). |
| Edit Domain | Allow actor to edit the Domain of an entity. |
| Edit Deprecation | Allow actor to edit the Deprecation status of an entity. |
-| Edit Assertions | Allow actor to add and remove assertions from an entity. |
-| Edit All | Allow actor to edit any information about an entity. Super user privileges. Controls the ability to ingest using API when REST API Authorization is enabled. |
+| Edit Lineage | Allow actor to edit custom lineage edges for the entity. |
+| Edit Data Product | Allow actor to edit the data product that an entity is part of |
+| Propose Tags | (Acryl DataHub only) Allow actor to propose new Tags for the entity. |
+| Propose Glossary Terms | (Acryl DataHub only) Allow actor to propose new Glossary Terms for the entity. |
+| Propose Documentation | (Acryl DataHub only) Allow actor to propose new Documentation for the entity. |
+| Manage Tag Proposals | (Acryl DataHub only) Allow actor to accept or reject proposed Tags for the entity. |
+| Manage Glossary Terms Proposals | (Acryl DataHub only) Allow actor to accept or reject proposed Glossary Terms for the entity. |
+| Manage Documentation Proposals | (Acryl DataHub only) Allow actor to accept or reject proposed Documentation for the entity |
+| Edit Entity | Allow actor to edit any information about an entity. Super user privileges. Controls the ability to ingest using API when REST API Authorization is enabled. |
| Get Timeline API[^1] | Allow actor to get the timeline of an entity via API. |
| Get Entity API[^1] | Allow actor to get an entity via API. |
| Get Timeseries Aspect API[^1] | Allow actor to get a timeseries aspect via API. |
@@ -225,10 +235,19 @@ The common Metadata Privileges, which span across entity types, include:
| Dataset | Edit Dataset Queries | Allow actor to edit the Highlighted Queries on the Queries tab of the dataset. |
| Dataset | View Dataset Usage | Allow actor to access usage metadata about a dataset both in the UI and in the GraphQL API. This includes example queries, number of queries, etc. Also applies to REST APIs when REST API Authorization is enabled. |
| Dataset | View Dataset Profile | Allow actor to access a dataset's profile both in the UI and in the GraphQL API. This includes snapshot statistics like #rows, #columns, null percentage per field, etc. |
+| Dataset | Edit Assertions | Allow actor to change the assertions associated with a dataset. |
+| Dataset | Edit Incidents | (Acryl DataHub only) Allow actor to change the incidents associated with a dataset. |
+| Dataset | Edit Monitors | (Acryl DataHub only) Allow actor to change the assertion monitors associated with a dataset. |
| Tag | Edit Tag Color | Allow actor to change the color of a Tag. |
| Group | Edit Group Members | Allow actor to add and remove members to a group. |
+| Group | Edit Contact Information | Allow actor to change email, slack handle associated with the group. |
+| Group | Manage Group Subscriptions | (Acryl DataHub only) Allow actor to subscribe the group to entities. |
+| Group | Manage Group Notifications | (Acryl DataHub only) Allow actor to change notification settings for the group. |
| User | Edit User Profile | Allow actor to change the user's profile including display name, bio, title, profile image, etc. |
| User + Group | Edit Contact Information | Allow actor to change the contact information such as email & chat handles. |
+| Term Group | Manage Direct Glossary Children | Allow actor to change the direct child Term Groups or Terms of the group. |
+| Term Group | Manage All Glossary Children | Allow actor to change any direct or indirect child Term Groups or Terms of the group. |
+
> **Still have questions about Privileges?** Let us know in [Slack](https://slack.datahubproject.io)!
diff --git a/docs/how/updating-datahub.md b/docs/how/updating-datahub.md
index 2b6fd5571cc9e..7ba516c82cf1b 100644
--- a/docs/how/updating-datahub.md
+++ b/docs/how/updating-datahub.md
@@ -15,6 +15,9 @@ This file documents any backwards-incompatible changes in DataHub and assists pe
- #8300: Clickhouse source now inherited from TwoTierSQLAlchemy. In old way we have platform_instance -> container -> co
container db (None) -> container schema and now we have platform_instance -> container database.
- #8300: Added `uri_opts` argument; now we can add any options for clickhouse client.
+- #8659: BigQuery ingestion no longer creates DataPlatformInstance aspects by default.
+ This will only affect users that were depending on this aspect for custom functionality,
+ and can be enabled via the `include_data_platform_instance` config option.
## 0.10.5
diff --git a/docs/lineage/airflow.md b/docs/lineage/airflow.md
index ef4071f89c585..21d59b777dd7c 100644
--- a/docs/lineage/airflow.md
+++ b/docs/lineage/airflow.md
@@ -62,6 +62,7 @@ lazy_load_plugins = False
| datahub.cluster | prod | name of the airflow cluster |
| datahub.capture_ownership_info | true | If true, the owners field of the DAG will be capture as a DataHub corpuser. |
| datahub.capture_tags_info | true | If true, the tags field of the DAG will be captured as DataHub tags. |
+ | datahub.capture_executions | true | If true, we'll capture task runs in DataHub in addition to DAG definitions. |
| datahub.graceful_exceptions | true | If set to true, most runtime errors in the lineage backend will be suppressed and will not cause the overall task to fail. Note that configuration issues will still throw exceptions. |
5. Configure `inlets` and `outlets` for your Airflow operators. For reference, look at the sample DAG in [`lineage_backend_demo.py`](../../metadata-ingestion/src/datahub_provider/example_dags/lineage_backend_demo.py), or reference [`lineage_backend_taskflow_demo.py`](../../metadata-ingestion/src/datahub_provider/example_dags/lineage_backend_taskflow_demo.py) if you're using the [TaskFlow API](https://airflow.apache.org/docs/apache-airflow/stable/concepts/taskflow.html).
@@ -80,9 +81,7 @@ Emitting DataHub ...
If you have created a custom Airflow operator [docs](https://airflow.apache.org/docs/apache-airflow/stable/howto/custom-operator.html) that inherits from the BaseOperator class,
when overriding the `execute` function, set inlets and outlets via `context['ti'].task.inlets` and `context['ti'].task.outlets`.
-The DataHub Airflow plugin will then pick up those inlets and outlets after the task runs.
-
-
+The DataHub Airflow plugin will then pick up those inlets and outlets after the task runs.
```python
class DbtOperator(BaseOperator):
@@ -97,8 +96,8 @@ class DbtOperator(BaseOperator):
def _get_lineage(self):
# Do some processing to get inlets/outlets
-
- return inlets, outlets
+
+ return inlets, outlets
```
If you override the `pre_execute` and `post_execute` function, ensure they include the `@prepare_lineage` and `@apply_lineage` decorators respectively. [source](https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/lineage.html#lineage)
@@ -172,7 +171,6 @@ Take a look at this sample DAG:
In order to use this example, you must first configure the Datahub hook. Like in ingestion, we support a Datahub REST hook and a Kafka-based hook. See step 1 above for details.
-
## Debugging
### Incorrect URLs
diff --git a/docs/modeling/extending-the-metadata-model.md b/docs/modeling/extending-the-metadata-model.md
index 32951ab2e41eb..f47630f44e772 100644
--- a/docs/modeling/extending-the-metadata-model.md
+++ b/docs/modeling/extending-the-metadata-model.md
@@ -323,7 +323,7 @@ It takes the following parameters:
annotations. To customize the set of analyzers used to index a certain field, you must add a new field type and define
the set of mappings to be applied in the MappingsBuilder.
- Thus far, we have implemented 10 fieldTypes:
+ Thus far, we have implemented 11 fieldTypes:
1. *KEYWORD* - Short text fields that only support exact matches, often used only for filtering
@@ -332,20 +332,25 @@ It takes the following parameters:
3. *TEXT_PARTIAL* - Text fields delimited by spaces/slashes/periods with partial matching support. Note, partial
matching is expensive, so this field type should not be applied to fields with long values (like description)
- 4. *BROWSE_PATH* - Field type for browse paths. Applies specific mappings for slash delimited paths.
+ 4. *WORD_GRAM* - Text fields delimited by spaces, slashes, periods, dashes, or underscores with partial matching AND
+ word gram support. That is, the text will be split by the delimiters and can be matched with delimited queries
+ matching two, three, or four length tokens in addition to single tokens. As with partial match, this type is
+ expensive, so should not be applied to fields with long values such as description.
- 5. *URN* - Urn fields where each sub-component inside the urn is indexed. For instance, for a data platform urn like
+ 5. *BROWSE_PATH* - Field type for browse paths. Applies specific mappings for slash delimited paths.
+
+ 6. *URN* - Urn fields where each sub-component inside the urn is indexed. For instance, for a data platform urn like
"urn:li:dataplatform:kafka", it will index the platform name "kafka" and ignore the common components
- 6. *URN_PARTIAL* - Urn fields where each sub-component inside the urn is indexed with partial matching support.
+ 7. *URN_PARTIAL* - Urn fields where each sub-component inside the urn is indexed with partial matching support.
- 7. *BOOLEAN* - Boolean fields used for filtering.
+ 8. *BOOLEAN* - Boolean fields used for filtering.
- 8. *COUNT* - Count fields used for filtering.
+ 9. *COUNT* - Count fields used for filtering.
- 9. *DATETIME* - Datetime fields used to represent timestamps.
+ 10. *DATETIME* - Datetime fields used to represent timestamps.
- 10. *OBJECT* - Each property in an object will become an extra column in Elasticsearch and can be referenced as
+ 11. *OBJECT* - Each property in an object will become an extra column in Elasticsearch and can be referenced as
`field.property` in queries. You should be careful to not use it on objects with many properties as it can cause a
mapping explosion in Elasticsearch.
diff --git a/docs/quickstart.md b/docs/quickstart.md
index b93713c4efa5c..cd91dc8d1ac84 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -145,6 +145,27 @@ Please refer to [Change the default user datahub in quickstart](authentication/c
We recommend deploying DataHub to production using Kubernetes. We provide helpful [Helm Charts](https://artifacthub.io/packages/helm/datahub/datahub) to help you quickly get up and running. Check out [Deploying DataHub to Kubernetes](./deploy/kubernetes.md) for a step-by-step walkthrough.
+The `quickstart` method of running DataHub is intended for local development and a quick way to experience the features that DataHub has to offer. It is not
+intended for a production environment. This recommendation is based on the following points.
+
+#### Default Credentials
+
+`quickstart` uses docker-compose configuration which includes default credentials for both DataHub, and it's underlying
+prerequisite data stores, such as MySQL. Additionally, other components are unauthenticated out of the box. This is a
+design choice to make development easier and is not best practice for a production environment.
+
+#### Exposed Ports
+
+DataHub's services, and it's backend data stores use the docker default behavior of binding to all interface addresses.
+This makes it useful for development but is not recommended in a production environment.
+
+#### Performance & Management
+
+* `quickstart` is limited by the resources available on a single host, there is no ability to scale horizontally.
+* Rollout of new versions requires downtime.
+* The configuration is largely pre-determined and not easily managed.
+* `quickstart`, by default, follows the most recent builds forcing updates to the latest released and unreleased builds.
+
## Other Common Operations
### Stopping DataHub
diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableFieldSpecExtractor.java b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableFieldSpecExtractor.java
index 2ffd9283ed456..8f2f42cd69cae 100644
--- a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableFieldSpecExtractor.java
+++ b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableFieldSpecExtractor.java
@@ -155,7 +155,8 @@ private void extractSearchableAnnotation(final Object annotationObj, final DataS
annotation.getBoostScore(),
annotation.getHasValuesFieldName(),
annotation.getNumValuesFieldName(),
- annotation.getWeightsPerFieldValue());
+ annotation.getWeightsPerFieldValue(),
+ annotation.getFieldNameAliases());
}
}
log.debug("Searchable annotation for field: {} : {}", schemaPathSpec, annotation);
diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableAnnotation.java b/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableAnnotation.java
index f2e65c771c6eb..d5e5044f95c23 100644
--- a/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableAnnotation.java
+++ b/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableAnnotation.java
@@ -4,7 +4,10 @@
import com.google.common.collect.ImmutableSet;
import com.linkedin.data.schema.DataSchema;
import com.linkedin.metadata.models.ModelValidationException;
+
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -19,9 +22,10 @@
@Value
public class SearchableAnnotation {
+ public static final String FIELD_NAME_ALIASES = "fieldNameAliases";
public static final String ANNOTATION_NAME = "Searchable";
private static final Set DEFAULT_QUERY_FIELD_TYPES =
- ImmutableSet.of(FieldType.TEXT, FieldType.TEXT_PARTIAL, FieldType.URN, FieldType.URN_PARTIAL);
+ ImmutableSet.of(FieldType.TEXT, FieldType.TEXT_PARTIAL, FieldType.WORD_GRAM, FieldType.URN, FieldType.URN_PARTIAL);
// Name of the field in the search index. Defaults to the field name in the schema
String fieldName;
@@ -47,6 +51,8 @@ public class SearchableAnnotation {
Optional numValuesFieldName;
// (Optional) Weights to apply to score for a given value
Map
*
* @param searchSourceBuilder {@link SearchSourceBuilder} that needs to be populated with sort order
@@ -187,13 +196,24 @@ public static void buildSortOrder(@Nonnull SearchSourceBuilder searchSourceBuild
final SortOrder esSortOrder =
(sortCriterion.getOrder() == com.linkedin.metadata.query.filter.SortOrder.ASCENDING) ? SortOrder.ASC
: SortOrder.DESC;
- searchSourceBuilder.sort(new FieldSortBuilder(sortCriterion.getField()).order(esSortOrder));
+ searchSourceBuilder.sort(new FieldSortBuilder(sortCriterion.getField()).order(esSortOrder).unmappedType(KEYWORD_TYPE));
}
if (sortCriterion == null || !sortCriterion.getField().equals(DEFAULT_SEARCH_RESULTS_SORT_BY_FIELD)) {
searchSourceBuilder.sort(new FieldSortBuilder(DEFAULT_SEARCH_RESULTS_SORT_BY_FIELD).order(SortOrder.ASC));
}
}
+ /**
+ * Populates source field of search query with the suggestions query so that we get search suggestions back.
+ * Right now we are only supporting suggestions based on the virtual _entityName field alias.
+ */
+ public static void buildNameSuggestions(@Nonnull SearchSourceBuilder searchSourceBuilder, @Nullable String textInput) {
+ SuggestionBuilder builder = SuggestBuilders.termSuggestion(ENTITY_NAME_FIELD).text(textInput);
+ SuggestBuilder suggestBuilder = new SuggestBuilder();
+ suggestBuilder.addSuggestion(NAME_SUGGESTION, builder);
+ searchSourceBuilder.suggest(suggestBuilder);
+ }
+
/**
* Escapes the Elasticsearch reserved characters in the given input string.
*
diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/SearchUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/SearchUtils.java
index 35a322d37b2fd..8b56ae0beb3f1 100644
--- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/SearchUtils.java
+++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/SearchUtils.java
@@ -78,7 +78,7 @@ public static Map getRequestMap(@Nullable Filter requestParams)
return criterionArray.stream().collect(Collectors.toMap(Criterion::getField, Criterion::getValue));
}
- static boolean isUrn(@Nonnull String value) {
+ public static boolean isUrn(@Nonnull String value) {
// TODO(https://github.com/datahub-project/datahub-gma/issues/51): This method is a bit of a hack to support searching for
// URNs that have commas in them, while also using commas a delimiter for search. We should stop supporting commas
// as delimiter, and then we can stop using this hack.
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/ESSampleDataFixture.java b/metadata-io/src/test/java/com/linkedin/metadata/ESSampleDataFixture.java
index 847029bc180eb..20501225ef787 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/ESSampleDataFixture.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/ESSampleDataFixture.java
@@ -54,6 +54,13 @@
@TestConfiguration
@Import(ESTestConfiguration.class)
public class ESSampleDataFixture {
+ /**
+ * Interested in adding more fixtures? Here's what you will need to update?
+ * 1. Create a new indexPrefix and FixtureName. Both are needed or else all fixtures will load on top of each other,
+ * overwriting each other
+ * 2. Create a new IndexConvention, IndexBuilder, and EntityClient. These are needed
+ * to index a different set of entities.
+ */
@Autowired
private ESBulkProcessor _bulkProcessor;
@@ -61,6 +68,9 @@ public class ESSampleDataFixture {
@Autowired
private RestHighLevelClient _searchClient;
+ @Autowired
+ private RestHighLevelClient _longTailSearchClient;
+
@Autowired
private SearchConfiguration _searchConfiguration;
@@ -68,24 +78,54 @@ public class ESSampleDataFixture {
private CustomSearchConfiguration _customSearchConfiguration;
@Bean(name = "sampleDataPrefix")
- protected String indexPrefix() {
+ protected String sampleDataPrefix() {
return "smpldat";
}
+ @Bean(name = "longTailPrefix")
+ protected String longTailIndexPrefix() {
+ return "lngtl";
+ }
+
@Bean(name = "sampleDataIndexConvention")
protected IndexConvention indexConvention(@Qualifier("sampleDataPrefix") String prefix) {
return new IndexConventionImpl(prefix);
}
+ @Bean(name = "longTailIndexConvention")
+ protected IndexConvention longTailIndexConvention(@Qualifier("longTailPrefix") String prefix) {
+ return new IndexConventionImpl(prefix);
+ }
+
@Bean(name = "sampleDataFixtureName")
- protected String fixtureName() {
+ protected String sampleDataFixtureName() {
return "sample_data";
}
+ @Bean(name = "longTailFixtureName")
+ protected String longTailFixtureName() {
+ return "long_tail";
+ }
+
@Bean(name = "sampleDataEntityIndexBuilders")
protected EntityIndexBuilders entityIndexBuilders(
@Qualifier("entityRegistry") EntityRegistry entityRegistry,
@Qualifier("sampleDataIndexConvention") IndexConvention indexConvention
+ ) {
+ return entityIndexBuildersHelper(entityRegistry, indexConvention);
+ }
+
+ @Bean(name = "longTailEntityIndexBuilders")
+ protected EntityIndexBuilders longTailEntityIndexBuilders(
+ @Qualifier("longTailEntityRegistry") EntityRegistry longTailEntityRegistry,
+ @Qualifier("longTailIndexConvention") IndexConvention indexConvention
+ ) {
+ return entityIndexBuildersHelper(longTailEntityRegistry, indexConvention);
+ }
+
+ protected EntityIndexBuilders entityIndexBuildersHelper(
+ EntityRegistry entityRegistry,
+ IndexConvention indexConvention
) {
GitVersion gitVersion = new GitVersion("0.0.0-test", "123456", Optional.empty());
ESIndexBuilder indexBuilder = new ESIndexBuilder(_searchClient, 1, 0, 1,
@@ -100,6 +140,23 @@ protected ElasticSearchService entitySearchService(
@Qualifier("entityRegistry") EntityRegistry entityRegistry,
@Qualifier("sampleDataEntityIndexBuilders") EntityIndexBuilders indexBuilders,
@Qualifier("sampleDataIndexConvention") IndexConvention indexConvention
+ ) throws IOException {
+ return entitySearchServiceHelper(entityRegistry, indexBuilders, indexConvention);
+ }
+
+ @Bean(name = "longTailEntitySearchService")
+ protected ElasticSearchService longTailEntitySearchService(
+ @Qualifier("longTailEntityRegistry") EntityRegistry longTailEntityRegistry,
+ @Qualifier("longTailEntityIndexBuilders") EntityIndexBuilders longTailEndexBuilders,
+ @Qualifier("longTailIndexConvention") IndexConvention longTailIndexConvention
+ ) throws IOException {
+ return entitySearchServiceHelper(longTailEntityRegistry, longTailEndexBuilders, longTailIndexConvention);
+ }
+
+ protected ElasticSearchService entitySearchServiceHelper(
+ EntityRegistry entityRegistry,
+ EntityIndexBuilders indexBuilders,
+ IndexConvention indexConvention
) throws IOException {
CustomConfiguration customConfiguration = new CustomConfiguration();
customConfiguration.setEnabled(true);
@@ -107,7 +164,7 @@ protected ElasticSearchService entitySearchService(
CustomSearchConfiguration customSearchConfiguration = customConfiguration.resolve(new YAMLMapper());
ESSearchDAO searchDAO = new ESSearchDAO(entityRegistry, _searchClient, indexConvention, false,
- ELASTICSEARCH_IMPLEMENTATION_ELASTICSEARCH, _searchConfiguration, customSearchConfiguration);
+ ELASTICSEARCH_IMPLEMENTATION_ELASTICSEARCH, _searchConfiguration, customSearchConfiguration);
ESBrowseDAO browseDAO = new ESBrowseDAO(entityRegistry, _searchClient, indexConvention, _searchConfiguration, _customSearchConfiguration);
ESWriteDAO writeDAO = new ESWriteDAO(entityRegistry, _searchClient, indexConvention, _bulkProcessor, 1);
return new ElasticSearchService(indexBuilders, searchDAO, browseDAO, writeDAO);
@@ -120,9 +177,30 @@ protected SearchService searchService(
@Qualifier("sampleDataEntitySearchService") ElasticSearchService entitySearchService,
@Qualifier("sampleDataEntityIndexBuilders") EntityIndexBuilders indexBuilders,
@Qualifier("sampleDataPrefix") String prefix,
- @Qualifier("sampleDataFixtureName") String fixtureName
+ @Qualifier("sampleDataFixtureName") String sampleDataFixtureName
) throws IOException {
+ return searchServiceHelper(entityRegistry, entitySearchService, indexBuilders, prefix, sampleDataFixtureName);
+ }
+ @Bean(name = "longTailSearchService")
+ @Nonnull
+ protected SearchService longTailSearchService(
+ @Qualifier("longTailEntityRegistry") EntityRegistry longTailEntityRegistry,
+ @Qualifier("longTailEntitySearchService") ElasticSearchService longTailEntitySearchService,
+ @Qualifier("longTailEntityIndexBuilders") EntityIndexBuilders longTailIndexBuilders,
+ @Qualifier("longTailPrefix") String longTailPrefix,
+ @Qualifier("longTailFixtureName") String longTailFixtureName
+ ) throws IOException {
+ return searchServiceHelper(longTailEntityRegistry, longTailEntitySearchService, longTailIndexBuilders, longTailPrefix, longTailFixtureName);
+ }
+
+ public SearchService searchServiceHelper(
+ EntityRegistry entityRegistry,
+ ElasticSearchService entitySearchService,
+ EntityIndexBuilders indexBuilders,
+ String prefix,
+ String fixtureName
+ ) throws IOException {
int batchSize = 100;
SearchRanker ranker = new SimpleRanker();
CacheManager cacheManager = new ConcurrentMapCacheManager();
@@ -159,6 +237,24 @@ protected EntityClient entityClient(
@Qualifier("sampleDataSearchService") SearchService searchService,
@Qualifier("sampleDataEntitySearchService") ElasticSearchService entitySearchService,
@Qualifier("entityRegistry") EntityRegistry entityRegistry
+ ) {
+ return entityClientHelper(searchService, entitySearchService, entityRegistry);
+ }
+
+ @Bean(name = "longTailEntityClient")
+ @Nonnull
+ protected EntityClient longTailEntityClient(
+ @Qualifier("sampleDataSearchService") SearchService searchService,
+ @Qualifier("sampleDataEntitySearchService") ElasticSearchService entitySearchService,
+ @Qualifier("longTailEntityRegistry") EntityRegistry longTailEntityRegistry
+ ) {
+ return entityClientHelper(searchService, entitySearchService, longTailEntityRegistry);
+ }
+
+ private EntityClient entityClientHelper(
+ SearchService searchService,
+ ElasticSearchService entitySearchService,
+ EntityRegistry entityRegistry
) {
CachingEntitySearchService cachingEntitySearchService = new CachingEntitySearchService(
new ConcurrentMapCacheManager(),
@@ -173,7 +269,7 @@ protected EntityClient entityClient(
preProcessHooks.setUiEnabled(true);
return new JavaEntityClient(
new EntityServiceImpl(mockAspectDao, null, entityRegistry, true, null,
- preProcessHooks),
+ preProcessHooks),
null,
entitySearchService,
cachingEntitySearchService,
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/ESTestConfiguration.java b/metadata-io/src/test/java/com/linkedin/metadata/ESTestConfiguration.java
index 0d7ac506599af..673474c96cc51 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/ESTestConfiguration.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/ESTestConfiguration.java
@@ -6,6 +6,7 @@
import com.linkedin.metadata.config.search.ExactMatchConfiguration;
import com.linkedin.metadata.config.search.PartialConfiguration;
import com.linkedin.metadata.config.search.SearchConfiguration;
+import com.linkedin.metadata.config.search.WordGramConfiguration;
import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration;
import com.linkedin.metadata.models.registry.ConfigEntityRegistry;
import com.linkedin.metadata.models.registry.EntityRegistry;
@@ -55,11 +56,17 @@ public SearchConfiguration searchConfiguration() {
exactMatchConfiguration.setCaseSensitivityFactor(0.7f);
exactMatchConfiguration.setEnableStructured(true);
+ WordGramConfiguration wordGramConfiguration = new WordGramConfiguration();
+ wordGramConfiguration.setTwoGramFactor(1.2f);
+ wordGramConfiguration.setThreeGramFactor(1.5f);
+ wordGramConfiguration.setFourGramFactor(1.8f);
+
PartialConfiguration partialConfiguration = new PartialConfiguration();
partialConfiguration.setFactor(0.4f);
partialConfiguration.setUrnFactor(0.5f);
searchConfiguration.setExactMatch(exactMatchConfiguration);
+ searchConfiguration.setWordGram(wordGramConfiguration);
searchConfiguration.setPartial(partialConfiguration);
return searchConfiguration;
}
@@ -137,4 +144,10 @@ public EntityRegistry entityRegistry() throws EntityRegistryException {
return new ConfigEntityRegistry(
ESTestConfiguration.class.getClassLoader().getResourceAsStream("entity-registry.yml"));
}
+
+ @Bean(name = "longTailEntityRegistry")
+ public EntityRegistry longTailEntityRegistry() throws EntityRegistryException {
+ return new ConfigEntityRegistry(
+ ESTestConfiguration.class.getClassLoader().getResourceAsStream("entity-registry.yml"));
+ }
}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/ESTestUtils.java b/metadata-io/src/test/java/com/linkedin/metadata/ESTestUtils.java
index 79496888650e1..45c4c16864b07 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/ESTestUtils.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/ESTestUtils.java
@@ -77,6 +77,11 @@ public static SearchResult searchAcrossEntities(SearchService searchService, Str
100, new SearchFlags().setFulltext(true).setSkipCache(true), facets);
}
+ public static SearchResult searchAcrossCustomEntities(SearchService searchService, String query, List searchableEntities) {
+ return searchService.searchAcrossEntities(searchableEntities, query, null, null, 0,
+ 100, new SearchFlags().setFulltext(true).setSkipCache(true));
+ }
+
public static SearchResult search(SearchService searchService, String query) {
return search(searchService, SEARCHABLE_ENTITIES, query);
}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/ElasticSearchGoldenTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/ElasticSearchGoldenTest.java
new file mode 100644
index 0000000000000..d720c95fef84d
--- /dev/null
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/ElasticSearchGoldenTest.java
@@ -0,0 +1,167 @@
+package com.linkedin.metadata.search.elasticsearch.fixtures;
+
+import com.linkedin.common.urn.Urn;
+import com.linkedin.datahub.graphql.generated.EntityType;
+import com.linkedin.datahub.graphql.resolvers.EntityTypeMapper;
+import com.linkedin.entity.client.EntityClient;
+import com.linkedin.metadata.ESSampleDataFixture;
+import com.linkedin.metadata.models.registry.EntityRegistry;
+import com.linkedin.metadata.search.MatchedFieldArray;
+import com.linkedin.metadata.search.SearchEntityArray;
+import com.linkedin.metadata.search.SearchResult;
+import com.linkedin.metadata.search.SearchService;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.annotations.Test;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.linkedin.metadata.ESTestUtils.*;
+import static org.testng.Assert.assertTrue;
+import static org.testng.AssertJUnit.*;
+
+@Import(ESSampleDataFixture.class)
+public class ElasticSearchGoldenTest extends AbstractTestNGSpringContextTests {
+
+ private static final List SEARCHABLE_LONGTAIL_ENTITIES = Stream.of(EntityType.CHART, EntityType.CONTAINER,
+ EntityType.DASHBOARD, EntityType.DATASET, EntityType.DOMAIN, EntityType.TAG
+ ).map(EntityTypeMapper::getName)
+ .collect(Collectors.toList());
+ @Autowired
+ private RestHighLevelClient _searchClient;
+
+ @Autowired
+ @Qualifier("longTailSearchService")
+ protected SearchService searchService;
+
+ @Autowired
+ @Qualifier("longTailEntityClient")
+ protected EntityClient entityClient;
+
+ @Autowired
+ @Qualifier("longTailEntityRegistry")
+ private EntityRegistry entityRegistry;
+
+ @Test
+ public void testNameMatchPetProfiles() {
+ /*
+ Searching for "pet profiles" should return "pet_profiles" as the first 2 search results
+ */
+ assertNotNull(searchService);
+ assertNotNull(entityRegistry);
+ SearchResult searchResult = searchAcrossCustomEntities(searchService, "pet profiles", SEARCHABLE_LONGTAIL_ENTITIES);
+ assertTrue(searchResult.getEntities().size() >= 2);
+ Urn firstResultUrn = searchResult.getEntities().get(0).getEntity();
+ Urn secondResultUrn = searchResult.getEntities().get(1).getEntity();
+
+ assertTrue(firstResultUrn.toString().contains("pet_profiles"));
+ assertTrue(secondResultUrn.toString().contains("pet_profiles"));
+ }
+
+ @Test
+ public void testNameMatchPetProfile() {
+ /*
+ Searching for "pet profile" should return "pet_profiles" as the first 2 search results
+ */
+ assertNotNull(searchService);
+ SearchResult searchResult = searchAcrossEntities(searchService, "pet profile", SEARCHABLE_LONGTAIL_ENTITIES);
+ assertTrue(searchResult.getEntities().size() >= 2);
+ Urn firstResultUrn = searchResult.getEntities().get(0).getEntity();
+ Urn secondResultUrn = searchResult.getEntities().get(1).getEntity();
+
+ assertTrue(firstResultUrn.toString().contains("pet_profiles"));
+ assertTrue(secondResultUrn.toString().contains("pet_profiles"));
+ }
+
+ @Test
+ public void testGlossaryTerms() {
+ /*
+ Searching for "ReturnRate" should return all tables that have the glossary term applied before
+ anything else
+ */
+ assertNotNull(searchService);
+ SearchResult searchResult = searchAcrossEntities(searchService, "ReturnRate", SEARCHABLE_LONGTAIL_ENTITIES);
+ SearchEntityArray entities = searchResult.getEntities();
+ assertTrue(searchResult.getEntities().size() >= 4);
+ MatchedFieldArray firstResultMatchedFields = entities.get(0).getMatchedFields();
+ MatchedFieldArray secondResultMatchedFields = entities.get(1).getMatchedFields();
+ MatchedFieldArray thirdResultMatchedFields = entities.get(2).getMatchedFields();
+ MatchedFieldArray fourthResultMatchedFields = entities.get(3).getMatchedFields();
+
+ assertTrue(firstResultMatchedFields.toString().contains("ReturnRate"));
+ assertTrue(secondResultMatchedFields.toString().contains("ReturnRate"));
+ assertTrue(thirdResultMatchedFields.toString().contains("ReturnRate"));
+ assertTrue(fourthResultMatchedFields.toString().contains("ReturnRate"));
+ }
+
+ @Test
+ public void testNameMatchPartiallyQualified() {
+ /*
+ Searching for "analytics.pet_details" (partially qualified) should return the fully qualified table
+ name as the first search results before any others
+ */
+ assertNotNull(searchService);
+ SearchResult searchResult = searchAcrossEntities(searchService, "analytics.pet_details", SEARCHABLE_LONGTAIL_ENTITIES);
+ assertTrue(searchResult.getEntities().size() >= 2);
+ Urn firstResultUrn = searchResult.getEntities().get(0).getEntity();
+ Urn secondResultUrn = searchResult.getEntities().get(1).getEntity();
+
+ assertTrue(firstResultUrn.toString().contains("snowflake,long_tail_companions.analytics.pet_details"));
+ assertTrue(secondResultUrn.toString().contains("dbt,long_tail_companions.analytics.pet_details"));
+ }
+
+ @Test
+ public void testNameMatchCollaborativeActionitems() {
+ /*
+ Searching for "collaborative actionitems" should return "collaborative_actionitems" as the first search
+ result, followed by "collaborative_actionitems_old"
+ */
+ assertNotNull(searchService);
+ SearchResult searchResult = searchAcrossEntities(searchService, "collaborative actionitems", SEARCHABLE_LONGTAIL_ENTITIES);
+ assertTrue(searchResult.getEntities().size() >= 2);
+ Urn firstResultUrn = searchResult.getEntities().get(0).getEntity();
+ Urn secondResultUrn = searchResult.getEntities().get(1).getEntity();
+
+ // Checks that the table name is not suffixed with anything
+ assertTrue(firstResultUrn.toString().contains("collaborative_actionitems,"));
+ assertTrue(secondResultUrn.toString().contains("collaborative_actionitems_old"));
+
+ Double firstResultScore = searchResult.getEntities().get(0).getScore();
+ Double secondResultScore = searchResult.getEntities().get(1).getScore();
+
+ // Checks that the scores aren't tied so that we are matching on table name more than column name
+ assertTrue(firstResultScore > secondResultScore);
+ }
+
+ @Test
+ public void testNameMatchCustomerOrders() {
+ /*
+ Searching for "customer orders" should return "customer_orders" as the first search
+ result, not suffixed by anything
+ */
+ assertNotNull(searchService);
+ SearchResult searchResult = searchAcrossEntities(searchService, "customer orders", SEARCHABLE_LONGTAIL_ENTITIES);
+ assertTrue(searchResult.getEntities().size() >= 2);
+ Urn firstResultUrn = searchResult.getEntities().get(0).getEntity();
+
+ // Checks that the table name is not suffixed with anything
+ assertTrue(firstResultUrn.toString().contains("customer_orders,"));
+
+ Double firstResultScore = searchResult.getEntities().get(0).getScore();
+ Double secondResultScore = searchResult.getEntities().get(1).getScore();
+
+ // Checks that the scores aren't tied so that we are matching on table name more than column name
+ assertTrue(firstResultScore > secondResultScore);
+ }
+
+ /*
+ Tests that should pass but do not yet can be added below here, with the following annotation:
+ @Test(enabled = false)
+ */
+
+}
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/SampleDataFixtureTests.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/SampleDataFixtureTests.java
index dada13bd6f479..d989d4ef4fa87 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/SampleDataFixtureTests.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/fixtures/SampleDataFixtureTests.java
@@ -82,6 +82,7 @@ public class SampleDataFixtureTests extends AbstractTestNGSpringContextTests {
protected EntityClient entityClient;
@Autowired
+ @Qualifier("entityRegistry")
private EntityRegistry entityRegistry;
@Test
@@ -357,6 +358,84 @@ public void testDelimitedSynonym() throws IOException {
}).collect(Collectors.toList());
}
+ @Test
+ public void testNegateAnalysis() throws IOException {
+ String queryWithMinus = "logging_events -bckp";
+ AnalyzeRequest request = AnalyzeRequest.withIndexAnalyzer(
+ "smpldat_datasetindex_v2",
+ "query_word_delimited", queryWithMinus
+ );
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()),
+ List.of("logging_events -bckp", "logging_ev", "-bckp", "log", "event", "bckp"));
+
+ request = AnalyzeRequest.withIndexAnalyzer(
+ "smpldat_datasetindex_v2",
+ "word_gram_3", queryWithMinus
+ );
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("logging events -bckp"));
+
+ request = AnalyzeRequest.withIndexAnalyzer(
+ "smpldat_datasetindex_v2",
+ "word_gram_4", queryWithMinus
+ );
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of());
+
+ }
+
+ @Test
+ public void testWordGram() throws IOException {
+ String text = "hello.cat_cool_customer";
+ AnalyzeRequest request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_2", text);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("hello cat", "cat cool", "cool customer"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_3", text);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("hello cat cool", "cat cool customer"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_4", text);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("hello cat cool customer"));
+
+ String testMoreSeparators = "quick.brown:fox jumped-LAZY_Dog";
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_2", testMoreSeparators);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()),
+ List.of("quick brown", "brown fox", "fox jumped", "jumped lazy", "lazy dog"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_3", testMoreSeparators);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()),
+ List.of("quick brown fox", "brown fox jumped", "fox jumped lazy", "jumped lazy dog"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_4", testMoreSeparators);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()),
+ List.of("quick brown fox jumped", "brown fox jumped lazy", "fox jumped lazy dog"));
+
+ String textWithQuotesAndDuplicateWord = "\"my_db.my_exact_table\"";
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_2", textWithQuotesAndDuplicateWord);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("my db", "db my", "my exact", "exact table"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_3", textWithQuotesAndDuplicateWord);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("my db my", "db my exact", "my exact table"));
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_4", textWithQuotesAndDuplicateWord);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("my db my exact", "db my exact table"));
+
+ String textWithParens = "(hi) there";
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", "word_gram_2", textWithParens);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of("hi there"));
+
+ String oneWordText = "hello";
+ for (String analyzer : List.of("word_gram_2", "word_gram_3", "word_gram_4")) {
+ request = AnalyzeRequest.withIndexAnalyzer("smpldat_datasetindex_v2", analyzer, oneWordText);
+ assertEquals(getTokens(request)
+ .map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toList()), List.of());
+ }
+ }
+
@Test
public void testUrnSynonym() throws IOException {
List expectedTokens = List.of("bigquery");
@@ -1266,6 +1345,53 @@ public void testParens() {
String.format("%s - Expected search results to include matched fields", query));
assertEquals(result.getEntities().size(), 2);
}
+ @Test
+ public void testGram() {
+ String query = "jaffle shop customers";
+ SearchResult result = searchAcrossEntities(searchService, query);
+ assertTrue(result.hasEntities() && !result.getEntities().isEmpty(),
+ String.format("%s - Expected search results", query));
+
+ assertEquals(result.getEntities().get(0).getEntity().toString(),
+ "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.customers,PROD)",
+ "Expected exact match in 1st position");
+
+ query = "shop customers source";
+ result = searchAcrossEntities(searchService, query);
+ assertTrue(result.hasEntities() && !result.getEntities().isEmpty(),
+ String.format("%s - Expected search results", query));
+
+ assertEquals(result.getEntities().get(0).getEntity().toString(),
+ "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.customers_source,PROD)",
+ "Expected ngram match in 1st position");
+
+ query = "jaffle shop stg customers";
+ result = searchAcrossEntities(searchService, query);
+ assertTrue(result.hasEntities() && !result.getEntities().isEmpty(),
+ String.format("%s - Expected search results", query));
+
+ assertEquals(result.getEntities().get(0).getEntity().toString(),
+ "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.stg_customers,PROD)",
+ "Expected ngram match in 1st position");
+
+ query = "jaffle shop transformers customers";
+ result = searchAcrossEntities(searchService, query);
+ assertTrue(result.hasEntities() && !result.getEntities().isEmpty(),
+ String.format("%s - Expected search results", query));
+
+ assertEquals(result.getEntities().get(0).getEntity().toString(),
+ "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.transformers_customers,PROD)",
+ "Expected ngram match in 1st position");
+
+ query = "shop raw customers";
+ result = searchAcrossEntities(searchService, query);
+ assertTrue(result.hasEntities() && !result.getEntities().isEmpty(),
+ String.format("%s - Expected search results", query));
+
+ assertEquals(result.getEntities().get(0).getEntity().toString(),
+ "urn:li:dataset:(urn:li:dataPlatform:dbt,cypress_project.jaffle_shop.raw_customers,PROD)",
+ "Expected ngram match in 1st position");
+ }
@Test
public void testPrefixVsExact() {
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilderTest.java
index ed72b46e98c46..0b33185549299 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilderTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilderTest.java
@@ -16,7 +16,7 @@ public void testMappingsBuilder() {
Map result = MappingsBuilder.getMappings(TestEntitySpecBuilder.getSpec());
assertEquals(result.size(), 1);
Map properties = (Map) result.get("properties");
- assertEquals(properties.size(), 17);
+ assertEquals(properties.size(), 19);
assertEquals(properties.get("urn"), ImmutableMap.of("type", "keyword",
"fields",
ImmutableMap.of("delimited",
@@ -66,6 +66,11 @@ public void testMappingsBuilder() {
assertTrue(textFieldSubfields.containsKey("delimited"));
assertTrue(textFieldSubfields.containsKey("keyword"));
+ // TEXT with addToFilters aliased under "_entityName"
+ Map textFieldAlias = (Map) properties.get("_entityName");
+ assertEquals(textFieldAlias.get("type"), "alias");
+ assertEquals(textFieldAlias.get("path"), "textFieldOverride");
+
// TEXT_PARTIAL
Map textArrayField = (Map) properties.get("textArrayField");
assertEquals(textArrayField.get("type"), "keyword");
@@ -76,6 +81,19 @@ public void testMappingsBuilder() {
assertTrue(textArrayFieldSubfields.containsKey("ngram"));
assertTrue(textArrayFieldSubfields.containsKey("keyword"));
+ // WORD_GRAM
+ Map wordGramField = (Map) properties.get("wordGramField");
+ assertEquals(wordGramField.get("type"), "keyword");
+ assertEquals(wordGramField.get("normalizer"), "keyword_normalizer");
+ Map wordGramFieldSubfields = (Map) wordGramField.get("fields");
+ assertEquals(wordGramFieldSubfields.size(), 6);
+ assertTrue(wordGramFieldSubfields.containsKey("delimited"));
+ assertTrue(wordGramFieldSubfields.containsKey("ngram"));
+ assertTrue(wordGramFieldSubfields.containsKey("keyword"));
+ assertTrue(wordGramFieldSubfields.containsKey("wordGrams2"));
+ assertTrue(wordGramFieldSubfields.containsKey("wordGrams3"));
+ assertTrue(wordGramFieldSubfields.containsKey("wordGrams4"));
+
// URN
Map foreignKey = (Map) properties.get("foreignKey");
assertEquals(foreignKey.get("type"), "text");
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/AggregationQueryBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/AggregationQueryBuilderTest.java
index 10b4ee42b1a71..36c8bb8f9a676 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/AggregationQueryBuilderTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/AggregationQueryBuilderTest.java
@@ -31,7 +31,8 @@ public void testGetDefaultAggregationsHasFields() {
1.0,
Optional.of("hasTest"),
Optional.empty(),
- Collections.emptyMap()
+ Collections.emptyMap(),
+ Collections.emptyList()
);
SearchConfiguration config = new SearchConfiguration();
@@ -60,7 +61,8 @@ public void testGetDefaultAggregationsFields() {
1.0,
Optional.empty(),
Optional.empty(),
- Collections.emptyMap()
+ Collections.emptyMap(),
+ Collections.emptyList()
);
SearchConfiguration config = new SearchConfiguration();
@@ -89,7 +91,8 @@ public void testGetSpecificAggregationsHasFields() {
1.0,
Optional.of("hasTest1"),
Optional.empty(),
- Collections.emptyMap()
+ Collections.emptyMap(),
+ Collections.emptyList()
);
SearchableAnnotation annotation2 = new SearchableAnnotation(
@@ -104,7 +107,8 @@ public void testGetSpecificAggregationsHasFields() {
1.0,
Optional.empty(),
Optional.empty(),
- Collections.emptyMap()
+ Collections.emptyMap(),
+ Collections.emptyList()
);
SearchConfiguration config = new SearchConfiguration();
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilderTest.java
index a2ec396c34b2d..282b1d8bb6778 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilderTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilderTest.java
@@ -4,6 +4,7 @@
import com.linkedin.metadata.config.search.ExactMatchConfiguration;
import com.linkedin.metadata.config.search.PartialConfiguration;
import com.linkedin.metadata.config.search.SearchConfiguration;
+import com.linkedin.metadata.config.search.WordGramConfiguration;
import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.google.common.collect.ImmutableList;
@@ -18,6 +19,7 @@
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.MatchPhrasePrefixQueryBuilder;
+import org.elasticsearch.index.query.MatchPhraseQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryStringQueryBuilder;
import org.elasticsearch.index.query.SimpleQueryStringBuilder;
@@ -46,11 +48,17 @@ public class SearchQueryBuilderTest {
exactMatchConfiguration.setCaseSensitivityFactor(0.7f);
exactMatchConfiguration.setEnableStructured(true);
+ WordGramConfiguration wordGramConfiguration = new WordGramConfiguration();
+ wordGramConfiguration.setTwoGramFactor(1.2f);
+ wordGramConfiguration.setThreeGramFactor(1.5f);
+ wordGramConfiguration.setFourGramFactor(1.8f);
+
PartialConfiguration partialConfiguration = new PartialConfiguration();
partialConfiguration.setFactor(0.4f);
partialConfiguration.setUrnFactor(0.7f);
testQueryConfig.setExactMatch(exactMatchConfiguration);
+ testQueryConfig.setWordGram(wordGramConfiguration);
testQueryConfig.setPartial(partialConfiguration);
}
public static final SearchQueryBuilder TEST_BUILDER = new SearchQueryBuilder(testQueryConfig, null);
@@ -70,16 +78,17 @@ public void testQueryBuilderFulltext() {
assertEquals(keywordQuery.value(), "testQuery");
assertEquals(keywordQuery.analyzer(), "keyword");
Map keywordFields = keywordQuery.fields();
- assertEquals(keywordFields.size(), 8);
+ assertEquals(keywordFields.size(), 9);
assertEquals(keywordFields, Map.of(
- "urn", 10.f,
- "textArrayField", 1.0f,
- "customProperties", 1.0f,
- "nestedArrayArrayField", 1.0f,
- "textFieldOverride", 1.0f,
- "nestedArrayStringField", 1.0f,
- "keyPart1", 10.0f,
- "esObjectField", 1.0f
+ "urn", 10.f,
+ "textArrayField", 1.0f,
+ "customProperties", 1.0f,
+ "wordGramField", 1.0f,
+ "nestedArrayArrayField", 1.0f,
+ "textFieldOverride", 1.0f,
+ "nestedArrayStringField", 1.0f,
+ "keyPart1", 10.0f,
+ "esObjectField", 1.0f
));
SimpleQueryStringBuilder urnComponentQuery = (SimpleQueryStringBuilder) analyzerGroupQuery.should().get(1);
@@ -99,7 +108,8 @@ public void testQueryBuilderFulltext() {
"nestedArrayArrayField.delimited", 0.4f,
"urn.delimited", 7.0f,
"textArrayField.delimited", 0.4f,
- "nestedArrayStringField.delimited", 0.4f
+ "nestedArrayStringField.delimited", 0.4f,
+ "wordGramField.delimited", 0.4f
));
BoolQueryBuilder boolPrefixQuery = (BoolQueryBuilder) shouldQueries.get(1);
@@ -109,21 +119,30 @@ public void testQueryBuilderFulltext() {
if (prefixQuery instanceof MatchPhrasePrefixQueryBuilder) {
MatchPhrasePrefixQueryBuilder builder = (MatchPhrasePrefixQueryBuilder) prefixQuery;
return Pair.of(builder.fieldName(), builder.boost());
- } else {
+ } else if (prefixQuery instanceof TermQueryBuilder) {
// exact
TermQueryBuilder builder = (TermQueryBuilder) prefixQuery;
return Pair.of(builder.fieldName(), builder.boost());
+ } else { // if (prefixQuery instanceof MatchPhraseQueryBuilder) {
+ // ngram
+ MatchPhraseQueryBuilder builder = (MatchPhraseQueryBuilder) prefixQuery;
+ return Pair.of(builder.fieldName(), builder.boost());
}
}).collect(Collectors.toList());
- assertEquals(prefixFieldWeights.size(), 22);
+ assertEquals(prefixFieldWeights.size(), 28);
List.of(
Pair.of("urn", 100.0f),
Pair.of("urn", 70.0f),
Pair.of("keyPart1.delimited", 16.8f),
Pair.of("keyPart1.keyword", 100.0f),
- Pair.of("keyPart1.keyword", 70.0f)
+ Pair.of("keyPart1.keyword", 70.0f),
+ Pair.of("wordGramField.wordGrams2", 1.44f),
+ Pair.of("wordGramField.wordGrams3", 2.25f),
+ Pair.of("wordGramField.wordGrams4", 3.2399998f),
+ Pair.of("wordGramField.keyword", 10.0f),
+ Pair.of("wordGramField.keyword", 7.0f)
).forEach(p -> assertTrue(prefixFieldWeights.contains(p), "Missing: " + p));
// Validate scorer
@@ -144,7 +163,7 @@ public void testQueryBuilderStructured() {
assertEquals(keywordQuery.queryString(), "testQuery");
assertNull(keywordQuery.analyzer());
Map keywordFields = keywordQuery.fields();
- assertEquals(keywordFields.size(), 16);
+ assertEquals(keywordFields.size(), 21);
assertEquals(keywordFields.get("keyPart1").floatValue(), 10.0f);
assertFalse(keywordFields.containsKey("keyPart3"));
assertEquals(keywordFields.get("textFieldOverride").floatValue(), 1.0f);
@@ -196,10 +215,14 @@ public void testCustomExactMatch() {
List queries = boolPrefixQuery.should().stream().map(prefixQuery -> {
if (prefixQuery instanceof MatchPhrasePrefixQueryBuilder) {
+ // prefix
return (MatchPhrasePrefixQueryBuilder) prefixQuery;
- } else {
+ } else if (prefixQuery instanceof TermQueryBuilder) {
// exact
return (TermQueryBuilder) prefixQuery;
+ } else { // if (prefixQuery instanceof MatchPhraseQueryBuilder) {
+ // ngram
+ return (MatchPhraseQueryBuilder) prefixQuery;
}
}).collect(Collectors.toList());
diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandlerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandlerTest.java
index d66d6a0ab0e76..db56e2d34881b 100644
--- a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandlerTest.java
+++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandlerTest.java
@@ -7,6 +7,7 @@
import com.linkedin.data.template.StringArray;
import com.linkedin.metadata.ESTestConfiguration;
import com.linkedin.metadata.TestEntitySpecBuilder;
+import com.linkedin.metadata.config.search.WordGramConfiguration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -65,11 +66,17 @@ public class SearchRequestHandlerTest extends AbstractTestNGSpringContextTests {
exactMatchConfiguration.setCaseSensitivityFactor(0.7f);
exactMatchConfiguration.setEnableStructured(true);
+ WordGramConfiguration wordGramConfiguration = new WordGramConfiguration();
+ wordGramConfiguration.setTwoGramFactor(1.2f);
+ wordGramConfiguration.setThreeGramFactor(1.5f);
+ wordGramConfiguration.setFourGramFactor(1.8f);
+
PartialConfiguration partialConfiguration = new PartialConfiguration();
partialConfiguration.setFactor(0.4f);
partialConfiguration.setUrnFactor(0.7f);
testQueryConfig.setExactMatch(exactMatchConfiguration);
+ testQueryConfig.setWordGram(wordGramConfiguration);
testQueryConfig.setPartial(partialConfiguration);
}
@@ -113,10 +120,10 @@ public void testSearchRequestHandler() {
HighlightBuilder highlightBuilder = sourceBuilder.highlighter();
List fields =
highlightBuilder.fields().stream().map(HighlightBuilder.Field::name).collect(Collectors.toList());
- assertEquals(fields.size(), 20);
+ assertEquals(fields.size(), 22);
List highlightableFields =
ImmutableList.of("keyPart1", "textArrayField", "textFieldOverride", "foreignKey", "nestedForeignKey",
- "nestedArrayStringField", "nestedArrayArrayField", "customProperties", "esObjectField");
+ "nestedArrayStringField", "nestedArrayArrayField", "customProperties", "esObjectField", "wordGramField");
highlightableFields.forEach(field -> {
assertTrue(fields.contains(field), "Missing: " + field);
assertTrue(fields.contains(field + ".*"), "Missing: " + field + ".*");
diff --git a/metadata-jobs/mae-consumer-job/build.gradle b/metadata-jobs/mae-consumer-job/build.gradle
index e7941a04224e3..3811a9537ac24 100644
--- a/metadata-jobs/mae-consumer-job/build.gradle
+++ b/metadata-jobs/mae-consumer-job/build.gradle
@@ -43,6 +43,8 @@ docker {
include 'docker/monitoring/*'
include "docker/${docker_repo}/*"
include 'metadata-models/src/main/resources/*'
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -55,7 +57,7 @@ tasks.getByName("docker").dependsOn([bootJar])
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/metadata-jobs/mce-consumer-job/build.gradle b/metadata-jobs/mce-consumer-job/build.gradle
index 5981284e9da3f..2229c387f3676 100644
--- a/metadata-jobs/mce-consumer-job/build.gradle
+++ b/metadata-jobs/mce-consumer-job/build.gradle
@@ -56,6 +56,8 @@ docker {
include 'docker/monitoring/*'
include "docker/${docker_repo}/*"
include 'metadata-models/src/main/resources/*'
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -68,7 +70,7 @@ tasks.getByName("docker").dependsOn([bootJar])
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}".toString())
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/chart/ChartInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/chart/ChartInfo.pdl
index 4339a186f1304..9fea71003ae6e 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/chart/ChartInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/chart/ChartInfo.pdl
@@ -20,8 +20,9 @@ record ChartInfo includes CustomProperties, ExternalReference {
* Title of the chart
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
- "enableAutocomplete": true
+ "fieldType": "WORD_GRAM",
+ "enableAutocomplete": true,
+ "fieldNameAliases": [ "_entityName" ]
}
title: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/container/ContainerProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/container/ContainerProperties.pdl
index 26745fe46caaa..526878cbe60d3 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/container/ContainerProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/container/ContainerProperties.pdl
@@ -15,9 +15,10 @@ record ContainerProperties includes CustomProperties, ExternalReference {
* Display name of the Asset Container
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
@@ -25,7 +26,7 @@ record ContainerProperties includes CustomProperties, ExternalReference {
* Fully-qualified name of the Container
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
@@ -61,4 +62,4 @@ record ContainerProperties includes CustomProperties, ExternalReference {
}
}
lastModified: optional TimeStamp
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dashboard/DashboardInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/dashboard/DashboardInfo.pdl
index 5cb306039506e..c436011eb58db 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dashboard/DashboardInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dashboard/DashboardInfo.pdl
@@ -22,9 +22,10 @@ record DashboardInfo includes CustomProperties, ExternalReference {
* Title of the dashboard
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
title: string
@@ -126,4 +127,4 @@ record DashboardInfo includes CustomProperties, ExternalReference {
* The time when this dashboard last refreshed
*/
lastRefreshed: optional Time
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/datajob/DataFlowInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/datajob/DataFlowInfo.pdl
index 481240740876a..2ff3e8cd930af 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/datajob/DataFlowInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/datajob/DataFlowInfo.pdl
@@ -17,9 +17,10 @@ record DataFlowInfo includes CustomProperties, ExternalReference {
* Flow name
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/datajob/DataJobInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/datajob/DataJobInfo.pdl
index 8737dd4d9ef52..250fb76003777 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/datajob/DataJobInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/datajob/DataJobInfo.pdl
@@ -18,9 +18,10 @@ record DataJobInfo includes CustomProperties, ExternalReference {
* Job name
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataplatform/DataPlatformInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataplatform/DataPlatformInfo.pdl
index acc40e9f693ec..5dd35c7f49520 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dataplatform/DataPlatformInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dataplatform/DataPlatformInfo.pdl
@@ -15,9 +15,10 @@ record DataPlatformInfo {
*/
@validate.strlen.max = 15
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": false,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
@@ -25,7 +26,7 @@ record DataPlatformInfo {
* The name that will be used for displaying a platform type.
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataplatforminstance/DataPlatformInstanceProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataplatforminstance/DataPlatformInstanceProperties.pdl
index d7ce5565103ee..b24e220ac3bcf 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dataplatforminstance/DataPlatformInstanceProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dataplatforminstance/DataPlatformInstanceProperties.pdl
@@ -16,9 +16,10 @@ record DataPlatformInstanceProperties includes CustomProperties, ExternalReferen
* Display name of the Data Platform Instance
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: optional string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataprocess/DataProcessInstanceProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataprocess/DataProcessInstanceProperties.pdl
index 72eefd5e294e4..c63cb1a97c017 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dataprocess/DataProcessInstanceProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dataprocess/DataProcessInstanceProperties.pdl
@@ -19,7 +19,7 @@ record DataProcessInstanceProperties includes CustomProperties, ExternalReferenc
* Process name
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
@@ -31,6 +31,7 @@ record DataProcessInstanceProperties includes CustomProperties, ExternalReferenc
@Searchable = {
"fieldType": "KEYWORD",
"addToFilters": true,
+ "fieldName": "processType",
"filterNameOverride": "Process Type"
}
type: optional enum DataProcessType {
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataproduct/DataProductProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataproduct/DataProductProperties.pdl
index 3861b7def7669..b2d26094fd0b7 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dataproduct/DataProductProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dataproduct/DataProductProperties.pdl
@@ -13,9 +13,10 @@ record DataProductProperties includes CustomProperties, ExternalReference {
* Display name of the Data Product
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: optional string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetProperties.pdl
index 57b1fe7693129..ad8705a29d4ed 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetProperties.pdl
@@ -17,9 +17,10 @@ record DatasetProperties includes CustomProperties, ExternalReference {
* Display name of the Dataset
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: optional string
@@ -27,7 +28,7 @@ record DatasetProperties includes CustomProperties, ExternalReference {
* Fully-qualified name of the Dataset
*/
@Searchable = {
- "fieldType": "TEXT",
+ "fieldType": "WORD_GRAM",
"addToFilters": false,
"enableAutocomplete": true,
"boostScore": 10.0
@@ -77,4 +78,4 @@ record DatasetProperties includes CustomProperties, ExternalReference {
*/
@deprecated = "Use GlobalTags aspect instead."
tags: array[string] = [ ]
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/domain/DomainProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/domain/DomainProperties.pdl
index 5a0b8657ecb47..5c8c8a4912e4c 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/domain/DomainProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/domain/DomainProperties.pdl
@@ -14,9 +14,10 @@ record DomainProperties {
* Display name of the Domain
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryNodeInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryNodeInfo.pdl
index 1e840e5a1df7e..c3388d4f462d4 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryNodeInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryNodeInfo.pdl
@@ -35,9 +35,10 @@ record GlossaryNodeInfo {
*/
@Searchable = {
"fieldName": "displayName",
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: optional string
@@ -49,4 +50,4 @@ record GlossaryNodeInfo {
}
id: optional string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryTermInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryTermInfo.pdl
index aa2a8b31e3dde..e987a71be7131 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryTermInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/glossary/GlossaryTermInfo.pdl
@@ -23,9 +23,10 @@ record GlossaryTermInfo includes CustomProperties {
* Display name of the term
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: optional string
@@ -75,4 +76,4 @@ record GlossaryTermInfo includes CustomProperties {
*/
@deprecated
rawSchema: optional string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpGroupInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpGroupInfo.pdl
index 8d764604237da..28b87476c61bd 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpGroupInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpGroupInfo.pdl
@@ -21,7 +21,8 @@ record CorpGroupInfo {
"fieldType": "TEXT_PARTIAL"
"queryByDefault": true,
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
displayName: optional string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl
index 6b050f484fedd..48ee53377e582 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl
@@ -45,7 +45,7 @@ record CorpUserEditableInfo {
* DataHub-native display name
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"queryByDefault": true,
"boostScore": 10.0
}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl
index 1cb705d426cc0..382b120fa942a 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserInfo.pdl
@@ -26,10 +26,11 @@ record CorpUserInfo includes CustomProperties {
* displayName of this user , e.g. Hang Zhang(DataHQ)
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"queryByDefault": true,
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
displayName: optional string
@@ -89,7 +90,7 @@ record CorpUserInfo includes CustomProperties {
* Common name of this user, format is firstName + lastName (split by a whitespace)
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"queryByDefault": true,
"enableAutocomplete": true,
"boostScore": 10.0
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpGroupKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpGroupKey.pdl
index 075cc14ddc83b..9e65b8f6e9929 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpGroupKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpGroupKey.pdl
@@ -11,10 +11,10 @@ record CorpGroupKey {
* The URL-encoded name of the AD/LDAP group. Serves as a globally unique identifier within DataHub.
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"queryByDefault": true,
"enableAutocomplete": true,
"boostScore": 10.0
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpUserKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpUserKey.pdl
index d1a8a4bb5bb23..476a0ad9704b3 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpUserKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/CorpUserKey.pdl
@@ -12,7 +12,7 @@ record CorpUserKey {
*/
@Searchable = {
"fieldName": "ldap",
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"boostScore": 2.0,
"enableAutocomplete": true
}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataFlowKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataFlowKey.pdl
index bcdb92f75d055..d8342630248b6 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataFlowKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataFlowKey.pdl
@@ -19,7 +19,7 @@ record DataFlowKey {
* Unique Identifier of the data flow
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true
}
flowId: string
@@ -31,4 +31,4 @@ record DataFlowKey {
"fieldType": "TEXT_PARTIAL"
}
cluster: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataJobKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataJobKey.pdl
index d0ac7dbca0f99..60ec51b464dcc 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataJobKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataJobKey.pdl
@@ -27,7 +27,7 @@ record DataJobKey {
* Unique Identifier of the data job
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true
}
jobId: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataProcessKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataProcessKey.pdl
index a5c05029352c2..4df1364a04ebe 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataProcessKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataProcessKey.pdl
@@ -13,7 +13,7 @@ record DataProcessKey {
* Process name i.e. an ETL job name
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 4.0
}
@@ -37,4 +37,4 @@ record DataProcessKey {
"queryByDefault": false
}
origin: FabricType
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DatasetKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DatasetKey.pdl
index ea1f9510ed438..70c5d174171af 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DatasetKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DatasetKey.pdl
@@ -25,7 +25,7 @@ record DatasetKey {
//This is no longer to be used for Dataset native name. Use name, qualifiedName from DatasetProperties instead.
@Searchable = {
"fieldName": "id"
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryNodeKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryNodeKey.pdl
index 88697fe3ff364..51a3bc00f4e9e 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryNodeKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryNodeKey.pdl
@@ -12,9 +12,9 @@ import com.linkedin.common.FabricType
record GlossaryNodeKey {
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryTermKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryTermKey.pdl
index a9f35146da18e..61bcd60cbc754 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryTermKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/GlossaryTermKey.pdl
@@ -13,10 +13,10 @@ record GlossaryTermKey {
* The term name, which serves as a unique id
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"fieldName": "id"
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureKey.pdl
index 579f1966977a9..050b954c89fb8 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureKey.pdl
@@ -20,9 +20,10 @@ record MLFeatureKey {
* Name of the feature
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 8.0
+ "boostScore": 8.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureTableKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureTableKey.pdl
index 1f786ad417be7..175a7b0d31b00 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureTableKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLFeatureTableKey.pdl
@@ -22,9 +22,10 @@ record MLFeatureTableKey {
* Name of the feature table
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 8.0
+ "boostScore": 8.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelDeploymentKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelDeploymentKey.pdl
index 7c36f410fede3..daa1deceb5fc3 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelDeploymentKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelDeploymentKey.pdl
@@ -19,9 +19,10 @@ record MLModelDeploymentKey {
* Name of the MLModelDeployment
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
@@ -35,4 +36,4 @@ record MLModelDeploymentKey {
"queryByDefault": false
}
origin: FabricType
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelGroupKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelGroupKey.pdl
index 17c401c0b8c48..582a899633c2a 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelGroupKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelGroupKey.pdl
@@ -19,9 +19,10 @@ record MLModelGroupKey {
* Name of the MLModelGroup
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
@@ -33,4 +34,4 @@ record MLModelGroupKey {
"queryByDefault": false
}
origin: FabricType
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelKey.pdl
index 55fd2bc370846..f097bbda738a2 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLModelKey.pdl
@@ -19,9 +19,10 @@ record MLModelKey {
* Name of the MLModel
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
@@ -35,4 +36,4 @@ record MLModelKey {
"queryByDefault": false
}
origin: FabricType
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLPrimaryKeyKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLPrimaryKeyKey.pdl
index 9eb67eaf5f651..ef812df206b46 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLPrimaryKeyKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/MLPrimaryKeyKey.pdl
@@ -21,9 +21,10 @@ record MLPrimaryKeyKey {
* Name of the primary key
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 8.0
+ "boostScore": 8.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/TagKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/TagKey.pdl
index 47f1a631b4a2c..4622e32dce67b 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/TagKey.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/TagKey.pdl
@@ -11,10 +11,10 @@ record TagKey {
* The tag name, which serves as a unique id
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0,
"fieldName": "id"
}
name: string
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/query/SearchFlags.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/query/SearchFlags.pdl
index 05a94b8fabc4b..be1a30c7f082c 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/query/SearchFlags.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/query/SearchFlags.pdl
@@ -28,4 +28,9 @@ record SearchFlags {
* Whether to skip aggregates/facets
*/
skipAggregates:optional boolean = false
+
+ /**
+ * Whether to request for search suggestions on the _entityName virtualized field
+ */
+ getSuggestions:optional boolean = false
}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchResultMetadata.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchResultMetadata.pdl
index 718d80ba4cb36..60f1b568f586a 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchResultMetadata.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchResultMetadata.pdl
@@ -12,4 +12,9 @@ record SearchResultMetadata {
*/
aggregations: array[AggregationMetadata] = []
+ /**
+ * A list of search query suggestions based on the given query
+ */
+ suggestions: array[SearchSuggestion] = []
+
}
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchSuggestion.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchSuggestion.pdl
new file mode 100644
index 0000000000000..7776ec54fe03e
--- /dev/null
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/search/SearchSuggestion.pdl
@@ -0,0 +1,24 @@
+namespace com.linkedin.metadata.search
+
+/**
+ * The model for the search result
+ */
+record SearchSuggestion {
+
+ /**
+ * The suggestion text for this search query
+ */
+ text: string
+
+ /**
+ * The score for how close this suggestion is to the original search query.
+ * The closer to 1 means it is closer to the original query and 0 is further away.
+ */
+ score: float
+
+ /**
+ * How many matches there are with the suggested text for the given field
+ */
+ frequency: long
+
+}
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/notebook/NotebookInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/notebook/NotebookInfo.pdl
index 1f4dcf975f48c..8ec5f262890f3 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/notebook/NotebookInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/notebook/NotebookInfo.pdl
@@ -18,9 +18,10 @@ record NotebookInfo includes CustomProperties, ExternalReference {
* Title of the Notebook
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
title: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/ownership/OwnershipTypeInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/ownership/OwnershipTypeInfo.pdl
index 004df6e399be4..3e7b53beff531 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/ownership/OwnershipTypeInfo.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/ownership/OwnershipTypeInfo.pdl
@@ -14,7 +14,7 @@ record OwnershipTypeInfo {
* Display name of the Ownership Type
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
@@ -54,4 +54,4 @@ record OwnershipTypeInfo {
}
}
lastModified: AuditStamp
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/query/QueryProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/query/QueryProperties.pdl
index bb7e22900e168..3ba19d348913b 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/query/QueryProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/query/QueryProperties.pdl
@@ -29,7 +29,7 @@ record QueryProperties {
* Optional display name to identify the query.
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0
}
@@ -69,4 +69,4 @@ record QueryProperties {
}
}
lastModified: AuditStamp
-}
\ No newline at end of file
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/role/RoleProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/role/RoleProperties.pdl
index acebdf5558c59..8422d3c49046c 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/role/RoleProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/role/RoleProperties.pdl
@@ -14,9 +14,10 @@ record RoleProperties {
* Display name of the IAM Role in the external system
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
diff --git a/metadata-models/src/main/pegasus/com/linkedin/tag/TagProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/tag/TagProperties.pdl
index 41c500c6fff2f..9df47fac3928a 100644
--- a/metadata-models/src/main/pegasus/com/linkedin/tag/TagProperties.pdl
+++ b/metadata-models/src/main/pegasus/com/linkedin/tag/TagProperties.pdl
@@ -11,9 +11,10 @@ record TagProperties {
* Display name of the tag
*/
@Searchable = {
- "fieldType": "TEXT_PARTIAL",
+ "fieldType": "WORD_GRAM",
"enableAutocomplete": true,
- "boostScore": 10.0
+ "boostScore": 10.0,
+ "fieldNameAliases": [ "_entityName" ]
}
name: string
diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DataHubAuthorizer.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DataHubAuthorizer.java
index 690528059b555..f653ccf72cf54 100644
--- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DataHubAuthorizer.java
+++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DataHubAuthorizer.java
@@ -250,11 +250,11 @@ private void addPoliciesToCache(final Map> cache
private void addPolicyToCache(final Map> cache, final DataHubPolicyInfo policy) {
final List privileges = policy.getPrivileges();
for (String privilege : privileges) {
- List existingPolicies = cache.getOrDefault(privilege, new ArrayList<>());
+ List existingPolicies = cache.containsKey(privilege) ? new ArrayList<>(cache.get(privilege)) : new ArrayList<>();
existingPolicies.add(policy);
cache.put(privilege, existingPolicies);
}
- List existingPolicies = cache.getOrDefault(ALL, new ArrayList<>());
+ List existingPolicies = cache.containsKey(ALL) ? new ArrayList<>(cache.get(ALL)) : new ArrayList<>();
existingPolicies.add(policy);
cache.put(ALL, existingPolicies);
}
diff --git a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/SearchResultVisualConfig.java b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/SearchResultVisualConfig.java
new file mode 100644
index 0000000000000..7094bbd710f75
--- /dev/null
+++ b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/SearchResultVisualConfig.java
@@ -0,0 +1,11 @@
+package com.linkedin.metadata.config;
+
+import lombok.Data;
+
+@Data
+public class SearchResultVisualConfig {
+ /**
+ * The default tab to show first on a Domain entity profile. Defaults to React code sorting if not present.
+ */
+ public Boolean enableNameHighlight;
+}
diff --git a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/VisualConfiguration.java b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/VisualConfiguration.java
index d1c357186e1ae..14ac2406c2256 100644
--- a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/VisualConfiguration.java
+++ b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/VisualConfiguration.java
@@ -22,4 +22,9 @@ public class VisualConfiguration {
* Queries tab related configurations
*/
public EntityProfileConfig entityProfile;
+
+ /**
+ * Search result related configurations
+ */
+ public SearchResultVisualConfig searchResult;
}
diff --git a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/SearchConfiguration.java b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/SearchConfiguration.java
index 1a56db1bd68b0..b2b5260dc5e70 100644
--- a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/SearchConfiguration.java
+++ b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/SearchConfiguration.java
@@ -11,4 +11,5 @@ public class SearchConfiguration {
private PartialConfiguration partial;
private CustomConfiguration custom;
private GraphQueryConfiguration graph;
+ private WordGramConfiguration wordGram;
}
diff --git a/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/WordGramConfiguration.java b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/WordGramConfiguration.java
new file mode 100644
index 0000000000000..624d2a4c63c4c
--- /dev/null
+++ b/metadata-service/configuration/src/main/java/com/linkedin/metadata/config/search/WordGramConfiguration.java
@@ -0,0 +1,11 @@
+package com.linkedin.metadata.config.search;
+
+import lombok.Data;
+
+
+@Data
+public class WordGramConfiguration {
+ private float twoGramFactor;
+ private float threeGramFactor;
+ private float fourGramFactor;
+}
diff --git a/metadata-service/configuration/src/main/resources/application.yml b/metadata-service/configuration/src/main/resources/application.yml
index 9f7bf92039fdc..d21442d0bf5c8 100644
--- a/metadata-service/configuration/src/main/resources/application.yml
+++ b/metadata-service/configuration/src/main/resources/application.yml
@@ -111,6 +111,8 @@ visualConfig:
entityProfile:
# we only support default tab for domains right now. In order to implement for other entities, update React code
domainDefaultTab: ${DOMAIN_DEFAULT_TAB:} # set to DOCUMENTATION_TAB to show documentation tab first
+ searchResult:
+ enableNameHighlight: ${SEARCH_RESULT_NAME_HIGHLIGHT_ENABLED:true} # Enables visual highlighting on search result names/descriptions.
# Storage Layer
@@ -198,6 +200,10 @@ elasticsearch:
prefixFactor: ${ELASTICSEARCH_QUERY_EXACT_MATCH_PREFIX_FACTOR:1.6} # boost multiplier when exact prefix
caseSensitivityFactor: ${ELASTICSEARCH_QUERY_EXACT_MATCH_CASE_FACTOR:0.7} # stacked boost multiplier when case mismatch
enableStructured: ${ELASTICSEARCH_QUERY_EXACT_MATCH_ENABLE_STRUCTURED:true} # enable exact match on structured search
+ wordGram:
+ twoGramFactor: ${ELASTICSEARCH_QUERY_TWO_GRAM_FACTOR:1.2} # boost multiplier when match on 2-gram tokens
+ threeGramFactor: ${ELASTICSEARCH_QUERY_THREE_GRAM_FACTOR:1.5} # boost multiplier when match on 3-gram tokens
+ fourGramFactor: ${ELASTICSEARCH_QUERY_FOUR_GRAM_FACTOR:1.8} # boost multiplier when match on 4-gram tokens
# Field weight annotations are typically calibrated for exact match, if partial match is possible on the field use these adjustments
partial:
urnFactor: ${ELASTICSEARCH_QUERY_PARTIAL_URN_FACTOR:0.5} # multiplier on Urn token match, a partial match on Urn > non-Urn is assumed
@@ -318,4 +324,4 @@ cache:
search:
lineage:
ttlSeconds: ${CACHE_SEARCH_LINEAGE_TTL_SECONDS:86400} # 1 day
- lightningThreshold: ${CACHE_SEARCH_LINEAGE_LIGHTNING_THRESHOLD:300}
\ No newline at end of file
+ lightningThreshold: ${CACHE_SEARCH_LINEAGE_LIGHTNING_THRESHOLD:300}
diff --git a/metadata-service/factories/build.gradle b/metadata-service/factories/build.gradle
index 796b6ee436b78..8e9b859e3b136 100644
--- a/metadata-service/factories/build.gradle
+++ b/metadata-service/factories/build.gradle
@@ -49,6 +49,12 @@ dependencies {
testCompile externalDependency.hazelcastTest
implementation externalDependency.jline
implementation externalDependency.common
+
+ constraints {
+ implementation(externalDependency.snappy) {
+ because("previous versions are vulnerable to CVE-2023-34453 through CVE-2023-34455")
+ }
+ }
}
configurations.all{
diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json
index 7aeca546af3c9..e3beef5ac4871 100644
--- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json
+++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json
@@ -341,6 +341,7 @@
"doc" : "Title of the chart",
"Searchable" : {
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1279,6 +1280,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1405,6 +1407,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1464,6 +1467,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1865,6 +1869,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -2061,6 +2066,7 @@
"boostScore" : 10.0,
"enableAutocomplete" : true,
"fieldName" : "displayName",
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -2097,6 +2103,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -2161,6 +2168,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2340,6 +2348,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -3217,6 +3226,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -3282,6 +3292,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -3867,6 +3878,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json
index 83ecaf41022c4..0c9b49649bf1e 100644
--- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json
+++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json
@@ -94,6 +94,7 @@
"doc" : "Title of the chart",
"Searchable" : {
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1326,6 +1327,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1471,6 +1473,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1530,6 +1533,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1922,6 +1926,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : false,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
},
"validate" : {
@@ -2111,6 +2116,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -2437,6 +2443,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2585,6 +2592,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -3704,6 +3712,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4302,6 +4311,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4390,6 +4400,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4484,6 +4495,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4590,6 +4602,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4696,6 +4709,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4796,6 +4810,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4879,6 +4894,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -5096,6 +5112,7 @@
"boostScore" : 10.0,
"enableAutocomplete" : true,
"fieldName" : "displayName",
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -5710,6 +5727,12 @@
"doc" : "Whether to skip aggregates/facets",
"default" : false,
"optional" : true
+ }, {
+ "name" : "getSuggestions",
+ "type" : "boolean",
+ "doc" : "Whether to request for search suggestions on the _entityName virtualized field",
+ "default" : false,
+ "optional" : true
} ]
}, {
"type" : "enum",
@@ -6081,6 +6104,31 @@
},
"doc" : "A list of search result metadata such as aggregations",
"default" : [ ]
+ }, {
+ "name" : "suggestions",
+ "type" : {
+ "type" : "array",
+ "items" : {
+ "type" : "record",
+ "name" : "SearchSuggestion",
+ "doc" : "The model for the search result",
+ "fields" : [ {
+ "name" : "text",
+ "type" : "string",
+ "doc" : "The suggestion text for this search query"
+ }, {
+ "name" : "score",
+ "type" : "float",
+ "doc" : "The score for how close this suggestion is to the original search query.\nThe closer to 1 means it is closer to the original query and 0 is further away."
+ }, {
+ "name" : "frequency",
+ "type" : "long",
+ "doc" : "How many matches there are with the suggested text for the given field"
+ } ]
+ }
+ },
+ "doc" : "A list of search query suggestions based on the given query",
+ "default" : [ ]
} ]
},
"doc" : "Metadata specific to the browse result of the queried path"
@@ -6187,7 +6235,7 @@
"type" : "int",
"doc" : "The total number of entities directly under searched path"
} ]
- }, "com.linkedin.metadata.search.SearchResultMetadata", "com.linkedin.metadata.snapshot.ChartSnapshot", "com.linkedin.metadata.snapshot.CorpGroupSnapshot", "com.linkedin.metadata.snapshot.CorpUserSnapshot", "com.linkedin.metadata.snapshot.DashboardSnapshot", "com.linkedin.metadata.snapshot.DataFlowSnapshot", "com.linkedin.metadata.snapshot.DataHubPolicySnapshot", "com.linkedin.metadata.snapshot.DataHubRetentionSnapshot", "com.linkedin.metadata.snapshot.DataJobSnapshot", "com.linkedin.metadata.snapshot.DataPlatformSnapshot", "com.linkedin.metadata.snapshot.DataProcessSnapshot", "com.linkedin.metadata.snapshot.DatasetSnapshot", "com.linkedin.metadata.snapshot.GlossaryNodeSnapshot", "com.linkedin.metadata.snapshot.GlossaryTermSnapshot", "com.linkedin.metadata.snapshot.MLFeatureSnapshot", "com.linkedin.metadata.snapshot.MLFeatureTableSnapshot", "com.linkedin.metadata.snapshot.MLModelDeploymentSnapshot", "com.linkedin.metadata.snapshot.MLModelGroupSnapshot", "com.linkedin.metadata.snapshot.MLModelSnapshot", "com.linkedin.metadata.snapshot.MLPrimaryKeySnapshot", "com.linkedin.metadata.snapshot.SchemaFieldSnapshot", "com.linkedin.metadata.snapshot.Snapshot", "com.linkedin.metadata.snapshot.TagSnapshot", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.DeploymentStatus", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLFeatureTableProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelDeploymentProperties", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelGroupProperties", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.MLPrimaryKeyProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", {
+ }, "com.linkedin.metadata.search.SearchResultMetadata", "com.linkedin.metadata.search.SearchSuggestion", "com.linkedin.metadata.snapshot.ChartSnapshot", "com.linkedin.metadata.snapshot.CorpGroupSnapshot", "com.linkedin.metadata.snapshot.CorpUserSnapshot", "com.linkedin.metadata.snapshot.DashboardSnapshot", "com.linkedin.metadata.snapshot.DataFlowSnapshot", "com.linkedin.metadata.snapshot.DataHubPolicySnapshot", "com.linkedin.metadata.snapshot.DataHubRetentionSnapshot", "com.linkedin.metadata.snapshot.DataJobSnapshot", "com.linkedin.metadata.snapshot.DataPlatformSnapshot", "com.linkedin.metadata.snapshot.DataProcessSnapshot", "com.linkedin.metadata.snapshot.DatasetSnapshot", "com.linkedin.metadata.snapshot.GlossaryNodeSnapshot", "com.linkedin.metadata.snapshot.GlossaryTermSnapshot", "com.linkedin.metadata.snapshot.MLFeatureSnapshot", "com.linkedin.metadata.snapshot.MLFeatureTableSnapshot", "com.linkedin.metadata.snapshot.MLModelDeploymentSnapshot", "com.linkedin.metadata.snapshot.MLModelGroupSnapshot", "com.linkedin.metadata.snapshot.MLModelSnapshot", "com.linkedin.metadata.snapshot.MLPrimaryKeySnapshot", "com.linkedin.metadata.snapshot.SchemaFieldSnapshot", "com.linkedin.metadata.snapshot.Snapshot", "com.linkedin.metadata.snapshot.TagSnapshot", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.DeploymentStatus", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLFeatureTableProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelDeploymentProperties", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelGroupProperties", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.MLPrimaryKeyProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", {
"type" : "record",
"name" : "SystemMetadata",
"namespace" : "com.linkedin.mxe",
diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json
index b1489df3db55e..ffaefc8232e83 100644
--- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json
+++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json
@@ -94,6 +94,7 @@
"doc" : "Title of the chart",
"Searchable" : {
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1032,6 +1033,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1158,6 +1160,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1217,6 +1220,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1618,6 +1622,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1806,6 +1811,7 @@
"boostScore" : 10.0,
"enableAutocomplete" : true,
"fieldName" : "displayName",
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1842,6 +1848,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1906,6 +1913,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2085,6 +2093,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2962,6 +2971,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -3027,6 +3037,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -3612,6 +3623,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json
index f4c2d16f84747..e385c7c30b21a 100644
--- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json
+++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json
@@ -94,6 +94,7 @@
"doc" : "Title of the chart",
"Searchable" : {
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1032,6 +1033,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1158,6 +1160,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1217,6 +1220,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1618,6 +1622,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1800,6 +1805,7 @@
"boostScore" : 10.0,
"enableAutocomplete" : true,
"fieldName" : "displayName",
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1836,6 +1842,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1900,6 +1907,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2079,6 +2087,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2956,6 +2965,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -3021,6 +3031,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -3606,6 +3617,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json
index 2676c2687bd72..b85c84be23795 100644
--- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json
+++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json
@@ -94,6 +94,7 @@
"doc" : "Title of the chart",
"Searchable" : {
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1326,6 +1327,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1471,6 +1473,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1530,6 +1533,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -1922,6 +1926,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : false,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
},
"validate" : {
@@ -2111,6 +2116,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -2431,6 +2437,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -2579,6 +2586,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL",
"queryByDefault" : true
}
@@ -3698,6 +3706,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4296,6 +4305,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4384,6 +4394,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4478,6 +4489,7 @@
"Searchable" : {
"boostScore" : 8.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
} ],
@@ -4584,6 +4596,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4690,6 +4703,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4790,6 +4804,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -4873,6 +4888,7 @@
"Searchable" : {
"boostScore" : 10.0,
"enableAutocomplete" : true,
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
@@ -5090,6 +5106,7 @@
"boostScore" : 10.0,
"enableAutocomplete" : true,
"fieldName" : "displayName",
+ "fieldNameAliases" : [ "_entityName" ],
"fieldType" : "TEXT_PARTIAL"
}
}, {
diff --git a/metadata-service/war/build.gradle b/metadata-service/war/build.gradle
index 7e9aa90664611..eaf14f7fd6c18 100644
--- a/metadata-service/war/build.gradle
+++ b/metadata-service/war/build.gradle
@@ -72,6 +72,8 @@ docker {
include 'docker/monitoring/*'
include "docker/${docker_repo}/*"
include 'metadata-models/src/main/resources/*'
+ }.exclude {
+ i -> i.file.isHidden() || i.file == buildDir
}
tag("Debug", "${docker_registry}/${docker_repo}:debug")
@@ -84,7 +86,7 @@ tasks.getByName("docker").dependsOn([build, war])
task cleanLocalDockerImages {
doLast {
- rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "v${version}")
+ rootProject.ext.cleanLocalDockerImages(docker_registry, docker_repo, "${version}")
}
}
dockerClean.finalizedBy(cleanLocalDockerImages)
diff --git a/metadata-service/war/src/main/resources/boot/policies.json b/metadata-service/war/src/main/resources/boot/policies.json
index 3fddf3456ecd7..3cda0269b79f1 100644
--- a/metadata-service/war/src/main/resources/boot/policies.json
+++ b/metadata-service/war/src/main/resources/boot/policies.json
@@ -19,6 +19,7 @@
"GENERATE_PERSONAL_ACCESS_TOKENS",
"MANAGE_ACCESS_TOKENS",
"MANAGE_DOMAINS",
+ "MANAGE_GLOBAL_ANNOUNCEMENTS",
"MANAGE_TESTS",
"MANAGE_GLOSSARIES",
"MANAGE_USER_CREDENTIALS",
@@ -102,6 +103,7 @@
"VIEW_ANALYTICS",
"GENERATE_PERSONAL_ACCESS_TOKENS",
"MANAGE_DOMAINS",
+ "MANAGE_GLOBAL_ANNOUNCEMENTS",
"MANAGE_TESTS",
"MANAGE_GLOSSARIES",
"MANAGE_TAGS",
@@ -190,6 +192,7 @@
"GENERATE_PERSONAL_ACCESS_TOKENS",
"MANAGE_ACCESS_TOKENS",
"MANAGE_DOMAINS",
+ "MANAGE_GLOBAL_ANNOUNCEMENTS",
"MANAGE_TESTS",
"MANAGE_GLOSSARIES",
"MANAGE_USER_CREDENTIALS",
@@ -283,6 +286,7 @@
"privileges":[
"GENERATE_PERSONAL_ACCESS_TOKENS",
"MANAGE_DOMAINS",
+ "MANAGE_GLOBAL_ANNOUNCEMENTS",
"MANAGE_GLOSSARIES",
"MANAGE_TAGS"
],
diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java
index c46d02a6eadf0..0b0d462f079bf 100644
--- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java
+++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java
@@ -64,6 +64,11 @@ public class PoliciesConfig {
"Manage Domains",
"Create and remove Asset Domains.");
+ public static final Privilege MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE = Privilege.of(
+ "MANAGE_GLOBAL_ANNOUNCEMENTS",
+ "Manage Home Page Posts",
+ "Create and delete home page posts");
+
public static final Privilege MANAGE_TESTS_PRIVILEGE = Privilege.of(
"MANAGE_TESTS",
"Manage Tests",
@@ -113,6 +118,7 @@ public class PoliciesConfig {
MANAGE_USERS_AND_GROUPS_PRIVILEGE,
VIEW_ANALYTICS_PRIVILEGE,
MANAGE_DOMAINS_PRIVILEGE,
+ MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE,
MANAGE_INGESTION_PRIVILEGE,
MANAGE_SECRETS_PRIVILEGE,
GENERATE_PERSONAL_ACCESS_TOKENS_PRIVILEGE,
@@ -192,8 +198,8 @@ public class PoliciesConfig {
public static final Privilege EDIT_ENTITY_PRIVILEGE = Privilege.of(
"EDIT_ENTITY",
- "Edit All",
- "The ability to edit any information about an entity. Super user privileges.");
+ "Edit Entity",
+ "The ability to edit any information about an entity. Super user privileges for the entity.");
public static final Privilege DELETE_ENTITY_PRIVILEGE = Privilege.of(
"DELETE_ENTITY",
diff --git a/smoke-test/run-quickstart.sh b/smoke-test/run-quickstart.sh
index 050b5d2db95c9..d40e4a5e7a4aa 100755
--- a/smoke-test/run-quickstart.sh
+++ b/smoke-test/run-quickstart.sh
@@ -15,4 +15,4 @@ echo "test_user:test_pass" >> ~/.datahub/plugins/frontend/auth/user.props
echo "DATAHUB_VERSION = $DATAHUB_VERSION"
DATAHUB_TELEMETRY_ENABLED=false \
DOCKER_COMPOSE_BASE="file://$( dirname "$DIR" )" \
-datahub docker quickstart --version ${DATAHUB_VERSION} --standalone_consumers --dump-logs-on-failure --kafka-setup
+datahub docker quickstart --version ${DATAHUB_VERSION} --standalone_consumers --dump-logs-on-failure --kafka-setup
\ No newline at end of file
diff --git a/smoke-test/tests/cypress/cypress/e2e/lineage/lineage_column_level.js b/smoke-test/tests/cypress/cypress/e2e/lineage/lineage_column_level.js
new file mode 100644
index 0000000000000..2a8fe045f154e
--- /dev/null
+++ b/smoke-test/tests/cypress/cypress/e2e/lineage/lineage_column_level.js
@@ -0,0 +1,51 @@
+const DATASET_ENTITY_TYPE = 'dataset';
+const DATASET_URN = 'urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)';
+
+describe("column-level lineage graph test", () => {
+
+ it("navigate to lineage graph view and verify that column-level lineage is showing correctly", () => {
+ cy.login();
+ cy.goToEntityLineageGraph(DATASET_ENTITY_TYPE, DATASET_URN);
+ //verify columns not shown by default
+ cy.waitTextVisible("SampleCypressHdfs");
+ cy.waitTextVisible("SampleCypressHive");
+ cy.waitTextVisible("cypress_logging");
+ cy.ensureTextNotPresent("shipment_info");
+ cy.ensureTextNotPresent("field_foo");
+ cy.ensureTextNotPresent("field_baz");
+ cy.ensureTextNotPresent("event_name");
+ cy.ensureTextNotPresent("event_data");
+ cy.ensureTextNotPresent("timestamp");
+ cy.ensureTextNotPresent("browser");
+ cy.clickOptionWithTestId("column-toggle")
+ //verify columns appear and belong co correct dataset
+ cy.waitTextVisible("shipment_info");
+ cy.waitTextVisible("shipment_info.date");
+ cy.waitTextVisible("shipment_info.target");
+ cy.waitTextVisible("shipment_info.destination");
+ cy.waitTextVisible("shipment_info.geo_info");
+ cy.waitTextVisible("field_foo");
+ cy.waitTextVisible("field_baz");
+ cy.waitTextVisible("event_name");
+ cy.waitTextVisible("event_data");
+ cy.waitTextVisible("timestamp");
+ cy.waitTextVisible("browser");
+ //verify columns can be hidden and shown again
+ cy.contains("Hide").click({ force:true });
+ cy.ensureTextNotPresent("field_foo");
+ cy.ensureTextNotPresent("field_baz");
+ cy.get("[aria-label='down']").eq(1).click({ force:true });
+ cy.waitTextVisible("field_foo");
+ cy.waitTextVisible("field_baz");
+ //verify columns can be disabled successfully
+ cy.clickOptionWithTestId("column-toggle")
+ cy.ensureTextNotPresent("shipment_info");
+ cy.ensureTextNotPresent("field_foo");
+ cy.ensureTextNotPresent("field_baz");
+ cy.ensureTextNotPresent("event_name");
+ cy.ensureTextNotPresent("event_data");
+ cy.ensureTextNotPresent("timestamp");
+ cy.ensureTextNotPresent("browser");
+ });
+
+});
\ No newline at end of file
diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/deprecations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/deprecations.js
index 1d41d155440e8..2fa11654a3c3e 100644
--- a/smoke-test/tests/cypress/cypress/e2e/mutations/deprecations.js
+++ b/smoke-test/tests/cypress/cypress/e2e/mutations/deprecations.js
@@ -1,19 +1,29 @@
-describe("deprecation", () => {
+describe("dataset deprecation", () => {
it("go to dataset and check deprecation works", () => {
const urn = "urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)";
const datasetName = "cypress_logging_events";
cy.login();
-
cy.goToDataset(urn, datasetName);
cy.openThreeDotDropdown();
cy.clickOptionWithText("Mark as deprecated");
cy.addViaFormModal("test deprecation", "Add Deprecation Details");
-
- cy.goToDataset(urn, datasetName);
- cy.contains("DEPRECATED");
-
+ cy.waitTextVisible("Deprecation Updated");
+ cy.waitTextVisible("DEPRECATED")
cy.openThreeDotDropdown();
cy.clickOptionWithText("Mark as un-deprecated");
+ cy.waitTextVisible("Deprecation Updated");
+ cy.ensureTextNotPresent("DEPRECATED");
+ cy.openThreeDotDropdown();
+ cy.clickOptionWithText("Mark as deprecated");
+ cy.addViaFormModal("test deprecation", "Add Deprecation Details");
+ cy.waitTextVisible("Deprecation Updated");
+ cy.waitTextVisible("DEPRECATED");
+ cy.contains("DEPRECATED").trigger("mouseover", { force: true });
+ cy.waitTextVisible("Deprecation note");
+ cy.get("[role='tooltip']").contains("Mark as un-deprecated").click();
+ cy.waitTextVisible("Confirm Mark as un-deprecated");
+ cy.get("button").contains("Yes").click();
+ cy.waitTextVisible("Marked assets as un-deprecated!");
cy.ensureTextNotPresent("DEPRECATED");
- });
+ });
});
diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js b/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js
new file mode 100644
index 0000000000000..e4e5a39ce1100
--- /dev/null
+++ b/smoke-test/tests/cypress/cypress/e2e/mutations/edit_documentation.js
@@ -0,0 +1,97 @@
+const test_id = Math.floor(Math.random() * 100000);
+const documentation_edited = `This is test${test_id} documentation EDITED`;
+const wrong_url = "https://www.linkedincom";
+const correct_url = "https://www.linkedin.com";
+
+describe("edit documentation and link to dataset", () => {
+ it("open test dataset page, edit documentation", () => {
+ //edit documentation and verify changes saved
+ cy.loginWithCredentials();
+ cy.visit(
+ "/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)/Schema"
+ );
+ cy.get("[role='tab']").contains("Documentation").click();
+ cy.waitTextVisible("my hive dataset");
+ cy.waitTextVisible("Sample doc");
+ cy.clickOptionWithText("Edit");
+ cy.focused().clear();
+ cy.focused().type(documentation_edited);
+ cy.get("button").contains("Save").click();
+ cy.waitTextVisible("Description Updated");
+ cy.waitTextVisible(documentation_edited);
+ //return documentation to original state
+ cy.clickOptionWithText("Edit");
+ cy.focused().clear().wait(1000);
+ cy.focused().type("my hive dataset");
+ cy.get("button").contains("Save").click();
+ cy.waitTextVisible("Description Updated");
+ cy.waitTextVisible("my hive dataset");
+ });
+
+ it("open test dataset page, remove and add dataset link", () => {
+ cy.loginWithCredentials();
+ cy.visit(
+ "/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)/Schema"
+ );
+ cy.get("[role='tab']").contains("Documentation").click();
+ cy.contains("Sample doc").trigger("mouseover", { force: true });
+ cy.get('[data-icon="delete"]').click();
+ cy.waitTextVisible("Link Removed");
+ cy.get("button").contains("Add Link").click();
+ cy.get("#addLinkForm_url").type(wrong_url);
+ cy.waitTextVisible("This field must be a valid url.");
+ cy.focused().clear();
+ cy.waitTextVisible("A URL is required.");
+ cy.focused().type(correct_url);
+ cy.ensureTextNotPresent("This field must be a valid url.");
+ cy.get("#addLinkForm_label").type("Sample doc");
+ cy.get('[role="dialog"] button').contains("Add").click();
+ cy.waitTextVisible("Link Added");
+ cy.get("[role='tab']").contains("Documentation").click();
+ cy.get(`[href='${correct_url}']`).should("be.visible");
+ });
+
+ it("open test domain page, remove and add dataset link", () => {
+ cy.loginWithCredentials();
+ cy.visit("/domain/urn:li:domain:marketing/Entities");
+ cy.get("[role='tab']").contains("Documentation").click();
+ cy.get("button").contains("Add Link").click();
+ cy.get("#addLinkForm_url").type(wrong_url);
+ cy.waitTextVisible("This field must be a valid url.");
+ cy.focused().clear();
+ cy.waitTextVisible("A URL is required.");
+ cy.focused().type(correct_url);
+ cy.ensureTextNotPresent("This field must be a valid url.");
+ cy.get("#addLinkForm_label").type("Sample doc");
+ cy.get('[role="dialog"] button').contains("Add").click();
+ cy.waitTextVisible("Link Added");
+ cy.get("[role='tab']").contains("Documentation").click();
+ cy.get(`[href='${correct_url}']`).should("be.visible");
+ cy.contains("Sample doc").trigger("mouseover", { force: true });
+ cy.get('[data-icon="delete"]').click();
+ cy.waitTextVisible("Link Removed");
+ });
+
+ it("edit field documentation", () => {
+ cy.loginWithCredentials();
+ cy.visit(
+ "/dataset/urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)/Schema"
+ );
+ cy.get("tbody [data-icon='edit']").first().click({ force: true });
+ cy.waitTextVisible("Update description");
+ cy.waitTextVisible("Foo field description has changed");
+ cy.focused().clear().wait(1000);
+ cy.focused().type(documentation_edited);
+ cy.get("button").contains("Update").click();
+ cy.waitTextVisible("Updated!");
+ cy.waitTextVisible(documentation_edited);
+ cy.waitTextVisible("(edited)");
+ cy.get("tbody [data-icon='edit']").first().click({ force: true });
+ cy.focused().clear().wait(1000);
+ cy.focused().type("Foo field description has changed");
+ cy.get("button").contains("Update").click();
+ cy.waitTextVisible("Updated!");
+ cy.waitTextVisible("Foo field description has changed");
+ cy.waitTextVisible("(edited)");
+ });
+});
diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js b/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js
index ddda8626fba2f..24a24cc21138d 100644
--- a/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js
+++ b/smoke-test/tests/cypress/cypress/e2e/mutations/managed_ingestion.js
@@ -31,8 +31,7 @@ describe("run managed ingestion", () => {
cy.waitTextVisible(testName)
cy.contains(testName).parent().within(() => {
- // TODO: Skipping until disk size resolved
- // cy.contains("Succeeded", {timeout: 30000})
+ cy.contains("Succeeded", {timeout: 180000})
cy.clickOptionWithTestId("delete-button");
})
cy.clickOptionWithText("Yes")
diff --git a/smoke-test/tests/cypress/cypress/e2e/settings/managing_groups.js b/smoke-test/tests/cypress/cypress/e2e/settings/managing_groups.js
index 7686acfe50de0..9559435ff01c8 100644
--- a/smoke-test/tests/cypress/cypress/e2e/settings/managing_groups.js
+++ b/smoke-test/tests/cypress/cypress/e2e/settings/managing_groups.js
@@ -64,6 +64,7 @@ describe("create and manage group", () => {
});
it("update group info", () => {
+ var expected_name = Cypress.env('ADMIN_USERNAME');
cy.loginWithCredentials();
cy.visit("/settings/identities/groups");
cy.clickOptionWithText(group_name);
@@ -77,13 +78,13 @@ describe("create and manage group", () => {
cy.contains("Test group description EDITED").should("be.visible");
cy.clickOptionWithText("Add Owners");
cy.contains("Search for users or groups...").click({ force: true });
- cy.focused().type(Cypress.env('ADMIN_USERNAME'));
- cy.get(".ant-select-item-option").contains(Cypress.env('ADMIN_USERNAME'), { matchCase: false }).click();
+ cy.focused().type(expected_name);
+ cy.get(".ant-select-item-option").contains(expected_name, { matchCase: false }).click();
cy.focused().blur();
- cy.contains(Cypress.env('ADMIN_USERNAME')).should("have.length", 1);
+ cy.contains(expected_name).should("have.length", 1);
cy.get('[role="dialog"] button').contains("Done").click();
cy.waitTextVisible("Owners Added");
- cy.contains(Cypress.env('ADMIN_USERNAME'), { matchCase: false }).should("be.visible");
+ cy.contains(expected_name, { matchCase: false }).should("be.visible");
cy.clickOptionWithText("Edit Group");
cy.waitTextVisible("Edit Profile");
cy.get("#email").type(`${test_id}@testemail.com`);
diff --git a/smoke-test/tests/cypress/data.json b/smoke-test/tests/cypress/data.json
index c6606519e8d73..3b2ee1afaba58 100644
--- a/smoke-test/tests/cypress/data.json
+++ b/smoke-test/tests/cypress/data.json
@@ -2012,4 +2012,4 @@
},
"systemMetadata": null
}
-]
+]
\ No newline at end of file
diff --git a/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl b/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl
index ed30244c31b17..6dff14133ee60 100644
--- a/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl
+++ b/test-models/src/main/pegasus/com/datahub/test/TestEntityInfo.pdl
@@ -14,7 +14,8 @@ record TestEntityInfo includes CustomProperties {
@Searchable = {
"fieldName": "textFieldOverride",
"fieldType": "TEXT",
- "addToFilters": true
+ "addToFilters": true,
+ "fieldNameAliases": [ "_entityName" ]
}
textField: optional string
@@ -25,6 +26,11 @@ record TestEntityInfo includes CustomProperties {
}
textArrayField: optional array[string]
+ @Searchable = {
+ "fieldType": "WORD_GRAM"
+ }
+ wordGramField: optional string
+
@Relationship = {
"name": "foreignKey",
"entityTypes": []