Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect, Page, test } from '@playwright/test';
import { redirectToHomePage } from '../../utils/common';
import { waitForAllLoadersToDisappear } from '../../utils/entity';

test.use({ storageState: 'playwright/.auth/admin.json' });

/**
* Navigates to MySQL service creation step 3 (configure connection),
* where the ServiceDocPanel is visible with code blocks and sections.
*/
const goToMysqlConnectionStep = async (page: Page, serviceName: string) => {
await page.goto('/databaseServices/add-service', {
waitUntil: 'domcontentloaded',
});
await waitForAllLoadersToDisappear(page);
await page.getByTestId('Mysql').click();
await page.getByTestId('next-button').click();
await page.getByTestId('service-name').fill(serviceName);
await page.getByTestId('next-button').click();
await page.getByTestId('service-requirements').waitFor({ state: 'visible' });
};

test.describe('ServiceDocPanel', () => {
test.beforeEach(async ({ page }) => {
await redirectToHomePage(page);
});

test.describe('Content rendering', () => {
test('should render headings not raw markdown', async ({ page }) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-headings');

const docPanel = page.getByTestId('service-requirements');

// Requirements h2 heading should render as an element, not raw "## Requirements"
await expect(docPanel.locator('h2').first()).toBeVisible();
await expect(docPanel).not.toContainText('## Requirements');
});

test('should render admonition blocks with correct class', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-admonition');

const docPanel = page.getByTestId('service-requirements');

// Mysql.md has $$note blocks — should render as .admonition.admonition-note
const admonition = docPanel.locator('.admonition-note').first();

await expect(admonition).toBeVisible();
// Should contain actual note content, not raw "$$note" syntax
await expect(docPanel).not.toContainText('$$note');
});

test('should render code blocks inside pre > code, not as raw text', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-codeblock');

const docPanel = page.getByTestId('service-requirements');

await expect(docPanel.locator('pre code').first()).toBeVisible();
// Raw fence markers should not appear
await expect(docPanel).not.toContainText('```');
});

test('should render links that open in a new tab', async ({ page }) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-links');

const docPanel = page.getByTestId('service-requirements');
const externalLink = docPanel.locator('a[target="_blank"]').first();

await expect(externalLink).toBeVisible();
await expect(externalLink).toHaveAttribute('href', /^https?:\/\//);
});

test('should render image in Mssql doc panel', async ({ page }) => {
await page.goto('/databaseServices/add-service', {
waitUntil: 'domcontentloaded',
});
await waitForAllLoadersToDisappear(page);
await page.getByTestId('Mssql').click();
await page.getByTestId('next-button').click();
await page.getByTestId('service-name').fill('pw-doc-panel-mssql-img');
await page.getByTestId('next-button').click();
await page.getByTestId('service-requirements').waitFor({
state: 'visible',
});

const docPanel = page.getByTestId('service-requirements');
const image = docPanel.locator('img').first();

await expect(image).toBeVisible();
// Verify the image loaded successfully (no broken image)
const naturalWidth = await image.evaluate(
(img: HTMLImageElement) => img.naturalWidth
);

expect(naturalWidth).toBeGreaterThan(0);
});
});

test.describe('Section highlighting', () => {
test('should highlight section when the corresponding form field is focused', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-highlight');

const docPanel = page.getByTestId('service-requirements');

// No section should be highlighted initially
await expect(
docPanel.locator('section[data-highlighted="true"]')
).toHaveCount(0);

// Focus the username field — activeField becomes "username"
await page.locator(String.raw`#root\/username`).focus();

// The username section should now be highlighted
const usernameSection = docPanel.locator(
'section[data-id="username"][data-highlighted="true"]'
);

await expect(usernameSection).toBeVisible();
});

test('should remove highlight from previous section when a new field is focused', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-highlight-switch');

const docPanel = page.getByTestId('service-requirements');

// Focus username first
await page.locator(String.raw`#root\/username`).focus();

await expect(
docPanel.locator('section[data-id="username"][data-highlighted="true"]')
).toBeVisible();

// Focus hostPort — username section should lose highlight
await page.locator(String.raw`#root\/hostPort`).focus();

await expect(
docPanel.locator('section[data-id="username"][data-highlighted="true"]')
).toHaveCount(0);

// hostPort section should now be highlighted
await expect(
docPanel.locator('section[data-id="hostPort"][data-highlighted="true"]')
).toBeVisible();
});

test('should only ever have one section highlighted at a time', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-single-highlight');

const docPanel = page.getByTestId('service-requirements');

await page.locator(String.raw`#root\/username`).focus();
await page.locator(String.raw`#root\/hostPort`).focus();

await expect(
docPanel.locator('section[data-highlighted="true"]')
).toHaveCount(1);
});

test('should load the correct doc file for the selected service type', async ({
page,
}) => {
await goToMysqlConnectionStep(page, 'pw-doc-panel-correct-doc');

const docPanel = page.getByTestId('service-requirements');

// MySQL doc starts with "# MySQL"
await expect(docPanel.locator('h1').first()).toContainText('MySQL');
});
});

test.describe('Code block copy button', () => {
test('should copy code block content to clipboard and show copied tooltip', async ({
page,
context,
}) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await goToMysqlConnectionStep(page, 'pw-doc-panel-copy');

const docPanel = page.getByTestId('service-requirements');
const codeBlock = docPanel.locator('pre').first();
const copyButtonWrapper = docPanel.locator('.code-copy-button').first();
const copyButton = docPanel.getByTestId('code-block-copy-icon').first();

// Hover code block to reveal the button
await codeBlock.hover();
await expect(copyButton).toBeVisible();

// Verify initial state
await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false');

// Click and verify copied state + tooltip
await copyButton.click();

await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'true');
await expect(page.getByRole('tooltip')).toBeVisible();

// Verify clipboard is non-empty
const clipboardText = await page.evaluate(() =>
navigator.clipboard.readText()
);

expect(clipboardText.length).toBeGreaterThan(0);

// Verify state resets after 2s timer
await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $$section
Source Python Class Name to instantiated by the ingestion workflow.

Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API.
$$

$$section
### Connection Options $(id="connectionOptions")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,21 @@ $$

If we choose to inform the GitHub credentials to ingest LookML Views:

$$section
#### Repository Owner $(id="repositoryOwner")

The owner (user or organization) of a GitHub repository. For example, in https://github.com/open-metadata/OpenMetadata, the owner is `open-metadata`.

$$

$$section
#### Repository Name $(id="repositoryName")

The name of a GitHub repository. For example, in https://github.com/open-metadata/OpenMetadata, the name is `OpenMetadata`.

$$

$$section
#### API Token $(id="token")

Token to use the API. This is required for private repositories and to ensure we don't hit API limits.
Expand All @@ -72,3 +79,5 @@ If your GitHub organization has SAML Single Sign-On (SSO) enabled, you must auth
Follow these <a href="https://docs.github.com/en/enterprise-cloud@latest/authentication/authenticating-with-single-sign-on/authorizing-a-personal-access-token-for-use-with-single-sign-on" target="_blank">steps</a> to authorize your token for use with SAML SSO.

$$

$$
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,9 @@ And is defined as:
```


{% note %}

$$note
If you have external services other than glue and facing permission issues, add the permissions to the list above.

{% /note %}
$$


You can find further information on the Athena connector in the <a href="https://docs.open-metadata.org/connectors/database/athena" target="_blank">docs</a>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $$section
Source Python Class Name to instantiated by the ingestion workflow.

Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API.
$$

$$section
### Connection Options $(id="connectionOptions")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ To extract basic metadata (catalogs, schemas, tables, views) from Databricks, th

```sql
-- Grant USE CATALOG on catalog
GRANT USE CATALOG ON CATALOG <catalog_name> TO `<user_or_service_principal>`;
GRANT USE CATALOG ON CATALOG <catalog_name> TO '<user_or_service_principal>';

-- Grant USE SCHEMA on schemas
GRANT USE SCHEMA ON SCHEMA <schema_name> TO `<user_or_service_principal>`;
GRANT USE SCHEMA ON SCHEMA <schema_name> TO '<user_or_service_principal>';

-- Grant SELECT on tables and views
GRANT SELECT ON TABLE <table_name> TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE <table_name> TO '<user_or_service_principal>';
```

### View Definitions (Optional)
Expand All @@ -31,7 +31,7 @@ To extract view definitions from `INFORMATION_SCHEMA.VIEWS`, ensure the user has

```sql
-- Grant SELECT on INFORMATION_SCHEMA.VIEWS
GRANT SELECT ON VIEW information_schema.views TO `<user_or_service_principal>`;
GRANT SELECT ON VIEW information_schema.views TO '<user_or_service_principal>';
```

### Unity Catalog Tags (Optional)
Expand All @@ -40,16 +40,16 @@ To extract tags at different levels (catalog, schema, table, column), the user n

```sql
-- For catalog-level tags
GRANT SELECT ON TABLE system.information_schema.catalog_tags TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.information_schema.catalog_tags TO '<user_or_service_principal>';

-- For schema-level tags
GRANT SELECT ON TABLE system.information_schema.schema_tags TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.information_schema.schema_tags TO '<user_or_service_principal>';

-- For table-level tags
GRANT SELECT ON TABLE system.information_schema.table_tags TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.information_schema.table_tags TO '<user_or_service_principal>';

-- For column-level tags
GRANT SELECT ON TABLE system.information_schema.column_tags TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.information_schema.column_tags TO '<user_or_service_principal>';
```

$$note
Expand All @@ -62,10 +62,10 @@ To extract table and column-level lineage from Unity Catalog system tables, the

```sql
-- For table lineage
GRANT SELECT ON TABLE system.access.table_lineage TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.access.table_lineage TO '<user_or_service_principal>';

-- For column lineage
GRANT SELECT ON TABLE system.access.column_lineage TO `<user_or_service_principal>`;
GRANT SELECT ON TABLE system.access.column_lineage TO '<user_or_service_principal>';
```

$$note
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,28 @@ $$section

In this configuration we will be pointing to the Hive Metastore database directly.

#### Hive Metastore Database ($id="metastoreDb")
### Hive Metastore Database

JDBC connection to the metastore database.

It should be a properly formatted database URL, which will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionURL`.

#### Connection UserName ($id="username")
#### Connection UserName

Username to use against the metastore database. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionUserName`.

#### Connection Password ($id="password")
#### Connection Password

Password to use against metastore database. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionPassword`.

#### Connection Driver Name ($id="driverName")
#### Connection Driver Name

Driver class name for JDBC metastore. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionDriverName`,
e.g., `org.mariadb.jdbc.Driver`.

You will need to provide the driver to the ingestion image, and pass the Class path as explained below.

#### JDBC Driver Class Path ($id="jdbcDriverClassPath")
#### JDBC Driver Class Path

Class path to JDBC driver required for the JDBC connection. The value will be used in the Spark Configuration under `spark.driver.extraClassPath`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ $$
$$section
### Connection Arguments $(id="connectionArguments")
Additional connection arguments such as security or protocol configs that can be sent to the service during connection.
$$
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ Uses Transport Layer Security (TLS) but disables the validation of the server ce
#### disable-tls
Does not use any Transport Layer Security (TLS). Data will be sent in plain text (no encryption).
While this may be helpful in rare cases of debugging, make sure you do not use this in production.
$$

Loading
Loading