feat(react): configure Cypress + MirageJS + GraphQL mock for functional testing plus a couple of example tests (#2597)

This commit is contained in:
Wan Chiu 2021-07-17 04:56:50 +10:00 committed by GitHub
parent 79e76e8b89
commit 49de7aba66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 3560 additions and 71 deletions

View File

@ -47,6 +47,20 @@ can run the following in this directory:
which will start a forwarding server at `localhost:3000`. Note that to fetch real data, `datahub-frontend` server will also
need to be deployed, still at `http://localhost:9002`, to service GraphQL API requests.
Optionally you could also start the app with the mock server without running the docker containers by executing `yarn start:mock`. See [here](src/graphql-mock/fixtures/searchResult/userSearchResult.ts#L6) for available login users.
### Functional testing
Automated functional testing is powered by Cypress and MirageJS. When running the web server with Cypress the port is set to 3010 so that the usual web server running on port 3000 used for development can be started without interruptions.
#### During development
`yarn test:e2e`
#### CI
`yarn test:e2e:ci`
### Theming
#### Selecting a theme

View File

@ -0,0 +1,4 @@
{
"baseUrl": "http://localhost:3010",
"video": false
}

View File

@ -0,0 +1,12 @@
module.exports = {
extends: '../.eslintrc.js',
parserOptions: {
project: 'cypress/tsconfig.json',
},
globals: {
Cypress: true,
},
rules: {
'jest/expect-expect': 0,
},
};

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,11 @@
export const login = (username) => {
cy.visit('/');
cy.get('input#username').type(username);
cy.get('input#password').type(username);
cy.contains('Log in').click();
};
export const logout = (username) => {
cy.get(`a[href="/user/urn:li:corpuser:${username}"]`).children('.anticon.anticon-caret-down').trigger('mouseover');
cy.get('li#user-profile-menu-logout').click();
};

View File

@ -0,0 +1,40 @@
import { createLoginUsers } from '../../src/graphql-mock/fixtures/user';
import { makeServer } from '../../src/graphql-mock/server';
import { login, logout } from '../helper/authHelper';
describe('Login', () => {
let server;
beforeEach(() => {
server = makeServer('test');
createLoginUsers(server);
});
afterEach(() => {
server.shutdown();
});
describe('given the login page is loaded', () => {
describe('when logging in with incorrect credentials', () => {
it('then the login should fail and the toast notification should be briefly displayed', () => {
login('kafkaa');
cy.contains('Failed to log in!').should('be.visible');
});
});
describe('when logging in with correct credentials', () => {
it('then the home page should be displayed', () => {
login('kafka');
cy.contains('Welcome back,').should('be.visible');
cy.contains('Datasets').should('be.visible');
cy.contains('Dashboard').should('be.visible');
cy.contains('Chart').should('be.visible');
cy.contains('Pipelines').should('be.visible');
logout('kafka');
});
});
});
});

View File

@ -0,0 +1,36 @@
import { createLoginUsers } from '../../src/graphql-mock/fixtures/user';
import { makeServer } from '../../src/graphql-mock/server';
import { login, logout } from '../helper/authHelper';
describe('Search', () => {
let server;
beforeEach(() => {
server = makeServer('test');
createLoginUsers(server);
});
afterEach(() => {
server.shutdown();
});
describe('given the home page is loaded', () => {
describe('when the user enters a keyword in the search field and results found and the first item is selected from the search result dropdown', () => {
it('then the search result page should be displayed with the Task tab be selected and the selected item be displayed', () => {
login('kafka');
cy.get('input[placeholder="Search Datasets, People, & more..."]').type('load');
cy.get('div.rc-virtual-list-holder-inner')
.children('div.ant-select-item.ant-select-item-option.ant-select-item-option-grouped')
.contains('load_all_')
.click();
cy.get('.ant-tabs-tab.ant-tabs-tab-active').contains('Task').should('be.visible');
cy.contains('load_all_').should('be.visible');
logout('kafka');
});
});
});
});

View File

@ -0,0 +1,47 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const findWebpack = require('find-webpack');
const wp = require('@cypress/webpack-preprocessor');
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// find the Webpack config used by react-scripts
const webpackOptions = findWebpack.getWebpackOptions();
if (!webpackOptions) {
throw new Error('Could not find Webpack in this project 😢');
}
const cleanOptions = {
reactScripts: true,
};
findWebpack.cleanForCypress(cleanOptions, webpackOptions);
const options = {
webpackOptions,
watchOptions: {},
};
on('file:preprocessor', wp(options));
// add other tasks to be registered here
// IMPORTANT to return the config object
// with the any changed environment variables
return config;
};

View File

@ -0,0 +1,20 @@
/* eslint-disable no-param-reassign */
Cypress.on('window:before:load', (win) => {
win.handleFromCypress = (request) => {
return fetch(request.url, {
method: request.method,
headers: request.requestHeaders,
body: request.requestBody,
})
.then((res) => {
const content = res.headers.get('content-type').includes('application/json') ? res.json() : res.text();
return new Promise((resolve) => {
content.then((body) => resolve([res.status, res.headers, body]));
});
})
.catch((error) => {
console.log('Cypress request proxy error', { error });
});
};
});

View File

@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"],
"noEmit": false
},
"include": ["../node_modules/cypress", "**/*.ts", "support/index.js"]
}

View File

@ -12,7 +12,9 @@
"@ant-design/icons": "^4.3.0",
"@apollo/client": "^3.3.19",
"@craco/craco": "^6.1.1",
"@cypress/webpack-preprocessor": "5.8.0",
"@data-ui/xy-chart": "^0.0.84",
"@miragejs/graphql": "^0.1.11",
"@react-hook/window-size": "^3.0.7",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
@ -43,14 +45,20 @@
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
"craco-antd": "^1.19.0",
"cypress": "7.3.0",
"d3-scale": "^3.3.0",
"d3-time-format": "^3.0.0",
"diff": "^5.0.0",
"dotenv": "^8.2.0",
"graphql": "^15.4.0",
"faker": "5.5.3",
"find-webpack": "2.2.1",
"graphql": "^15.5.0",
"graphql-tag": "2.10.3",
"graphql.macro": "^1.4.2",
"history": "^5.0.0",
"js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8",
"miragejs": "^0.1.41",
"query-string": "^6.13.8",
"rc-table": "^7.13.1",
"react": "^17.0.0",
@ -60,6 +68,7 @@
"react-router-dom": "^5.1.6",
"react-scripts": "4.0.3",
"sinon": "^11.1.1",
"start-server-and-test": "1.12.2",
"styled-components": "^5.2.1",
"typescript": "^4.1.3",
"uuid": "^8.3.2",
@ -67,10 +76,17 @@
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "yarn run generate && BROWSER=none craco start",
"start": "yarn run generate && BROWSER=none REACT_APP_MOCK=false craco start",
"start:mock": "yarn run generate && BROWSER=none REACT_APP_MOCK=true craco start",
"start:e2e": "REACT_APP_MOCK=cy BROWSER=none PORT=3010 craco start",
"ec2-dev": "yarn run generate && CI=true;export CI;BROWSER=none craco start",
"build": "yarn run generate && CI=false craco build && rm -rf dist/ && cp -r build/ dist/ && rm -r build/",
"build": "yarn run generate && CI=false REACT_APP_MOCK=false craco build && rm -rf dist/ && cp -r build/ dist/ && rm -r build/",
"test": "craco test",
"cy:run:ci": "cypress run",
"pretest:e2e:ci": "yarn generate",
"test:e2e:ci": "start-server-and-test start:e2e 3010 cy:run:ci",
"cy:open": "cypress open",
"test:e2e": "start-server-and-test start:e2e 3010 cy:open",
"eject": "react-scripts eject",
"generate": "graphql-codegen --config codegen.yml",
"lint": "eslint . --ext .ts,.tsx --quiet",
@ -112,9 +128,9 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
"http-proxy-middleware": "2.0.0",
"prettier": "^2.3.0"
},
"proxy": "http://localhost:9002",
"resolutions": {
"@ant-design/colors": "5.0.0"
}

View File

@ -3,12 +3,10 @@ import Cookies from 'js-cookie';
import { BrowserRouter as Router } from 'react-router-dom';
import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache, ServerError } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { MockedProvider } from '@apollo/client/testing';
import { ThemeProvider } from 'styled-components';
import './App.less';
import { Routes } from './app/Routes';
import { mocks } from './Mocks';
import EntityRegistry from './app/entity/EntityRegistry';
import { DashboardEntity } from './app/entity/dashboard/DashboardEntity';
import { ChartEntity } from './app/entity/chart/ChartEntity';
@ -26,11 +24,8 @@ import { isLoggedInVar } from './app/auth/checkAuthStatus';
import { GlobalCfg } from './conf';
import { GlossaryTermEntity } from './app/entity/glossaryTerm/GlossaryTermEntity';
// Enable to use the Apollo MockProvider instead of a real HTTP client
const MOCK_MODE = false;
/*
Construct Apollo Client
Construct Apollo Client
*/
const httpLink = createHttpLink({ uri: '/api/v2/graphql' });
@ -112,23 +107,9 @@ const App: React.VFC = () => {
<ThemeProvider theme={dynamicThemeConfig}>
<Router>
<EntityRegistryContext.Provider value={entityRegistry}>
{/* Temporary: For local testing during development. */}
{MOCK_MODE ? (
<MockedProvider
mocks={mocks}
addTypename={false}
defaultOptions={{
watchQuery: { fetchPolicy: 'no-cache' },
query: { fetchPolicy: 'no-cache' },
}}
>
<Routes />
</MockedProvider>
) : (
<ApolloProvider client={client}>
<Routes />
</ApolloProvider>
)}
<ApolloProvider client={client}>
<Routes />
</ApolloProvider>
</EntityRegistryContext.Provider>
</Router>
</ThemeProvider>

View File

@ -64,7 +64,9 @@ export const AllEntitiesSearchResults = ({ query }: Props) => {
{Object.keys(allSearchResultsByType).map((type: any) => {
const searchResults = allSearchResultsByType[type].data?.search?.searchResults;
if (searchResults && searchResults.length > 0) {
return <EntityGroupSearchResults type={type} query={query} searchResults={searchResults} />;
return (
<EntityGroupSearchResults key={type} type={type} query={query} searchResults={searchResults} />
);
}
return null;
})}

View File

@ -65,7 +65,7 @@ export const ManageAccount = ({ urn: _urn, pictureLink: _pictureLink, name }: Pr
</MenuItem>
);
})}
<MenuItem danger key="logout" onClick={handleLogout} tabIndex={0}>
<MenuItem id="user-profile-menu-logout" danger key="logout" onClick={handleLogout} tabIndex={0}>
Log out
</MenuItem>
</Menu>

View File

@ -0,0 +1,12 @@
/* eslint-disable global-require */
/* eslint-disable @typescript-eslint/no-var-requires */
if (process.env.REACT_APP_MOCK === 'true' || process.env.REACT_APP_MOCK === 'cy') {
if (process.env.REACT_APP_MOCK === 'cy') {
require('./server').makeServerForCypress();
} else {
require('./server').makeServer();
}
}
export {};

View File

@ -0,0 +1,13 @@
import { EntityType } from '../../../types.generated';
import { BrowsePathResolver } from '../browsePathHelper';
import { chartBrowsePaths, filterChartByPath } from '../searchResult/chartSearchResult';
const browsePathResolver = new BrowsePathResolver({
entityType: EntityType.Chart,
paths: chartBrowsePaths,
filterEntityHandler: filterChartByPath,
});
export default {
...browsePathResolver.getBrowse(),
};

View File

@ -0,0 +1,13 @@
import { EntityType } from '../../../types.generated';
import { BrowsePathResolver } from '../browsePathHelper';
import { dashboardBrowsePaths, filterDashboardByPath } from '../searchResult/dashboardSearchResult';
const browsePathResolver = new BrowsePathResolver({
entityType: EntityType.Dashboard,
paths: dashboardBrowsePaths,
filterEntityHandler: filterDashboardByPath,
});
export default {
...browsePathResolver.getBrowse(),
};

View File

@ -0,0 +1,13 @@
import { EntityType } from '../../../types.generated';
import { BrowsePathResolver } from '../browsePathHelper';
import { dataFlowBrowsePaths, filterDataFlowByPath } from '../searchResult/dataFlowSearchResult';
const browsePathResolver = new BrowsePathResolver({
entityType: EntityType.DataFlow,
paths: dataFlowBrowsePaths,
filterEntityHandler: filterDataFlowByPath,
});
export default {
...browsePathResolver.getBrowse(),
};

View File

@ -0,0 +1,13 @@
import { EntityType } from '../../../types.generated';
import { BrowsePathResolver } from '../browsePathHelper';
import { datasetBrowsePaths, filterDatasetByPath } from '../searchResult/datasetSearchResult';
const browsePathResolver = new BrowsePathResolver({
entityType: EntityType.Dataset,
paths: datasetBrowsePaths,
filterEntityHandler: filterDatasetByPath,
});
export default {
...browsePathResolver.getBrowse(),
};

View File

@ -0,0 +1,159 @@
import { BrowseInput, BrowseResultGroup, BrowseResults, Entity, EntityType, SearchResult } from '../../types.generated';
import { toLowerCaseEntityType, toTitleCase } from '../helper';
import { EntityBrowseFn, EntityBrowsePath, GetBrowseResults, StringNumber } from '../types';
type ToFlatPathsArg = {
flatPaths: StringNumber[][];
paths: EntityBrowsePath[];
parentPaths: string[];
};
export const toFlatPaths = ({ flatPaths, paths, parentPaths }: ToFlatPathsArg) => {
paths.forEach(({ name, paths: childPaths, count = 0 }) => {
if (childPaths.length) {
parentPaths.push(name);
toFlatPaths({ flatPaths, parentPaths, paths: childPaths });
} else {
flatPaths.push([...parentPaths, name, count]);
}
});
parentPaths.pop();
};
type FilterEntityByPathArg = {
term: string;
searchResults: SearchResult[];
};
export const filterEntityByPath = ({ term, searchResults }: FilterEntityByPathArg): Entity[] => {
return searchResults
.filter((r) => {
const regex = new RegExp(term);
return regex.test(r.entity.urn);
})
.map((r) => r.entity);
};
export class BrowsePathResolver {
private readonly browse: Record<string, EntityBrowseFn>;
private readonly paths: EntityBrowsePath[];
private readonly filterEntityHandler: (path: string[]) => Entity[];
private readonly baseBrowseResult: GetBrowseResults = {
data: {
browse: {
entities: [],
start: 0,
count: 0,
total: 0,
metadata: {
path: [],
groups: [],
totalNumEntities: 0,
__typename: 'BrowseResultMetadata',
},
__typename: 'BrowseResults',
},
},
};
constructor({
entityType,
paths,
filterEntityHandler,
}: {
entityType: EntityType;
paths: EntityBrowsePath[];
filterEntityHandler(path: string[]): Entity[];
}) {
this.browse = {};
this.paths = paths;
this.filterEntityHandler = filterEntityHandler;
const browsePathKey = `${toLowerCaseEntityType(entityType)}Browse`;
const groups = this.paths.map<BrowseResultGroup>(({ name, paths: rootPaths, count = 0 }) => ({
name,
count: rootPaths.length ? rootPaths.reduce(this.sumTotalEntityByPaths, 0) : count,
__typename: 'BrowseResultGroup',
}));
this.initBrowsePathResolver({ browsePathKey, groups });
this.initBrowsePathResolverForPaths({ prefixPathKey: browsePathKey, paths });
}
public getBrowse() {
return this.browse;
}
private initBrowsePathResolver({ browsePathKey, groups }: { browsePathKey: string; groups: BrowseResultGroup[] }) {
if (!this.browse.hasOwnProperty(browsePathKey)) {
const dataBrowse: BrowseResults = JSON.parse(JSON.stringify(this.baseBrowseResult.data.browse));
Object.assign(this.browse, {
[browsePathKey]: ({ start, count, path }: BrowseInput): GetBrowseResults => {
const startValue = start as number;
const countValue = count as number;
const paths = path as string[];
const entities = groups.length ? [] : this.filterEntityHandler(paths);
const chunkEntities = entities.slice(startValue, startValue + countValue);
return {
data: {
browse: {
...dataBrowse,
entities: chunkEntities,
start: startValue,
count: chunkEntities.length,
total: entities.length,
metadata: {
...dataBrowse.metadata,
path: paths,
groups,
totalNumEntities: groups.reduce(this.sumTotalEntityByGroups, 0),
},
},
},
};
},
});
}
}
private initBrowsePathResolverForPaths({
prefixPathKey,
paths,
}: {
prefixPathKey: string;
paths: EntityBrowsePath[];
}) {
paths.forEach(({ name, paths: childPaths }) => {
const browsePathKey = `${prefixPathKey}${toTitleCase(name)}`;
if (childPaths.length) {
const groups = childPaths.map<BrowseResultGroup>(
({ name: childName, paths: child2Paths, count = 0 }) => ({
name: childName,
count: child2Paths.length ? child2Paths.reduce(this.sumTotalEntityByPaths, 0) : count,
__typename: 'BrowseResultGroup',
}),
);
this.initBrowsePathResolver({ browsePathKey, groups });
this.initBrowsePathResolverForPaths({ prefixPathKey: browsePathKey, paths: childPaths });
} else {
this.initBrowsePathResolver({ browsePathKey, groups: [] });
}
});
}
private sumTotalEntityByGroups = (out: number, { count = 0 }: BrowseResultGroup): number => {
return out + count;
};
private sumTotalEntityByPaths = (out: number, { paths, count = 0 }: EntityBrowsePath): number => {
if (paths.length) {
return paths.reduce(this.sumTotalEntityByPaths, out);
}
return out + count;
};
}

View File

@ -0,0 +1,41 @@
import * as faker from 'faker';
import { Chart, ChartType, EntityType, OwnershipType } from '../../../types.generated';
import { findUserByUsername } from '../searchResult/userSearchResult';
export const chartEntity = (tool): Chart => {
const name = `${faker.company.bsNoun()}_${faker.company.bsNoun()}_${faker.company.bsNoun()}`;
const description = `${faker.commerce.productDescription()}`;
const datahubUser = findUserByUsername('datahub');
return {
urn: `urn:li:chart:(${tool},${name})`,
type: EntityType.Chart,
tool,
chartId: '2',
info: {
name,
description,
externalUrl:
'https://superset.demo.datahubproject.io/superset/explore/?form_data=%7B%22slice_id%22%3A%202%7D',
type: ChartType.Pie,
access: null,
lastModified: { time: 1619137330, __typename: 'AuditStamp' },
created: { time: 1619137330, __typename: 'AuditStamp' },
__typename: 'ChartInfo',
},
editableProperties: null,
ownership: {
owners: [
{
owner: datahubUser,
type: OwnershipType.Stakeholder,
__typename: 'Owner',
},
],
lastModified: { time: 1619717962718, __typename: 'AuditStamp' },
__typename: 'Ownership',
},
globalTags: null,
__typename: 'Chart',
};
};

View File

@ -0,0 +1,87 @@
import * as faker from 'faker';
import { generateTag } from '../tag';
import { Dashboard, EntityType, Ownership, OwnershipType } from '../../../types.generated';
import { findUserByUsername } from '../searchResult/userSearchResult';
export const dashboardEntity = (tool): Dashboard => {
const name = `${faker.company.bsNoun()}`;
const description = `${faker.commerce.productDescription()}`;
const datahubUser = findUserByUsername('datahub');
const kafkaUser = findUserByUsername('kafka');
const lookerUser = findUserByUsername('looker');
const datahubOwnership: Ownership = {
owners: [
{
owner: datahubUser,
type: OwnershipType.Stakeholder,
__typename: 'Owner',
},
],
lastModified: { time: 1619993818664, __typename: 'AuditStamp' },
__typename: 'Ownership',
};
const kafkaOwnership: Ownership = {
owners: [
{
owner: kafkaUser,
type: OwnershipType.Stakeholder,
__typename: 'Owner',
},
],
lastModified: { time: 1619993818664, __typename: 'AuditStamp' },
__typename: 'Ownership',
};
return {
urn: `urn:li:dashboard:(${tool},${name})`,
type: EntityType.Dashboard,
tool,
dashboardId: '3',
editableProperties: null,
info: {
name,
description,
externalUrl: null,
access: null,
charts: [],
lastModified: { time: 1619160920, __typename: 'AuditStamp' },
created: { time: 1619160920, __typename: 'AuditStamp' },
__typename: 'DashboardInfo',
},
ownership: {
owners: [
{
owner: datahubUser,
type: OwnershipType.Stakeholder,
__typename: 'Owner',
},
{
owner: kafkaUser,
type: OwnershipType.Developer,
__typename: 'Owner',
},
{
owner: lookerUser,
type: OwnershipType.Developer,
__typename: 'Owner',
},
],
lastModified: { time: 1619993818664, __typename: 'AuditStamp' },
__typename: 'Ownership',
},
globalTags: {
tags: [
{
tag: generateTag(datahubOwnership),
__typename: 'TagAssociation',
},
{
tag: generateTag(kafkaOwnership),
__typename: 'TagAssociation',
},
],
__typename: 'GlobalTags',
},
__typename: 'Dashboard',
};
};

View File

@ -0,0 +1,43 @@
import * as faker from 'faker';
import { DataFlow, EntityType, OwnershipType } from '../../../types.generated';
import { findUserByUsername } from '../searchResult/userSearchResult';
export type DataFlowEntityArg = {
orchestrator: string;
cluster: string;
};
export const dataFlowEntity = ({ orchestrator, cluster }: DataFlowEntityArg): DataFlow => {
const flowId = `${faker.company.bsNoun()}_${faker.company.bsNoun()}`;
const description = `${faker.commerce.productDescription()}`;
const datahubUser = findUserByUsername('datahub');
return {
urn: `urn:li:dataFlow:(${orchestrator},${flowId},${cluster})`,
type: EntityType.DataFlow,
orchestrator,
flowId,
cluster,
info: {
name: flowId,
description,
project: null,
__typename: 'DataFlowInfo',
},
editableProperties: null,
ownership: {
owners: [
{
owner: datahubUser,
type: OwnershipType.Stakeholder,
__typename: 'Owner',
},
],
lastModified: { time: 1620224528712, __typename: 'AuditStamp' },
__typename: 'Ownership',
},
globalTags: { tags: [], __typename: 'GlobalTags' },
dataJobs: null,
__typename: 'DataFlow',
};
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
import * as faker from 'faker';
import { DataPlatform, Dataset, EntityType, FabricType, OwnershipType, PlatformType } from '../../../types.generated';
import kafkaLogo from '../../../images/kafkalogo.png';
import s3Logo from '../../../images/s3.png';
import snowflakeLogo from '../../../images/snowflakelogo.png';
import bigqueryLogo from '../../../images/bigquerylogo.png';
import { findUserByUsername } from '../searchResult/userSearchResult';
const platformLogo = {
kafka: kafkaLogo,
s3: s3Logo,
snowflake: snowflakeLogo,
bigquery: bigqueryLogo,
};
const generatePlatform = ({ platform, urn }): DataPlatform => {
return {
urn,
type: EntityType.Dataset,
name: platform,
info: {
type: PlatformType.Others,
datasetNameDelimiter: '',
logoUrl: platformLogo[platform],
__typename: 'DataPlatformInfo',
},
__typename: 'DataPlatform',
};
};
export type DatasetEntityArg = {
platform: string;
origin: FabricType;
path: string;
};
export const datasetEntity = ({
platform,
origin,
path,
}: DatasetEntityArg): Dataset & { previousSchemaMetadata: any } => {
const name = `${path}.${faker.company.bsNoun()}_${faker.company.bsNoun()}`;
const description = `${faker.commerce.productDescription()}`;
const datahubUser = findUserByUsername('datahub');
const platformURN = `urn:li:dataPlatform:${platform}`;
return {
urn: `urn:li:dataset:(${platformURN},${name},${origin.toUpperCase()})`,
type: EntityType.Dataset,
name,
origin,
description,
uri: null,
platform: generatePlatform({ platform, urn: platformURN }),
platformNativeType: null,
tags: [],
properties: null,
editableProperties: null,
editableSchemaMetadata: null,
deprecation: null,
ownership: {
owners: [
{
owner: datahubUser,
type: OwnershipType.Dataowner,
__typename: 'Owner',
},
],
lastModified: {
time: 1616107219521,
__typename: 'AuditStamp',
},
__typename: 'Ownership',
},
globalTags: {
tags: [],
__typename: 'GlobalTags',
},
institutionalMemory: null,
usageStats: null,
glossaryTerms: null,
schemaMetadata: null,
previousSchemaMetadata: null,
__typename: 'Dataset',
};
};

View File

@ -0,0 +1,38 @@
import * as faker from 'faker';
import { CorpUser, EntityType } from '../../../types.generated';
export type UserEntityArg = {
username: string;
};
export const userEntity = (option?: UserEntityArg): CorpUser => {
const username = option?.username || `${faker.internet.userName()}`;
const title = `${faker.name.title()}`;
const firstName = `${faker.name.firstName()}`;
const lastName = `${faker.name.lastName()}`;
const email = `${faker.internet.email()}`;
return {
urn: `urn:li:corpuser:${username}`,
type: EntityType.CorpUser,
username,
info: {
active: true,
displayName: `${firstName} ${lastName}`,
title,
email,
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
__typename: 'CorpUserInfo',
},
editableInfo: {
aboutMe: `about ${username}`,
teams: [faker.company.companyName(), faker.company.companyName()],
skills: [faker.company.catchPhrase(), faker.company.catchPhrase()],
pictureLink: null,
__typename: 'CorpUserEditableInfo',
},
__typename: 'CorpUser',
};
};

View File

@ -0,0 +1,5 @@
export * from './searchResult';
export { default as datasetBrowseResult } from './browseDataset';
export { default as dashboardBrowseResult } from './browseDashboard';
export { default as chartBrowseResult } from './browseChart';
export { default as dataflowBrowseResult } from './browseDataFlow';

View File

@ -0,0 +1,68 @@
import { Chart, SearchResult, SearchResults } from '../../../types.generated';
import { EntityBrowsePath } from '../../types';
import { filterEntityByPath } from '../browsePathHelper';
import { chartEntity } from '../entity/chartEntity';
import { generateData } from './dataGenerator';
const searchResult = (tool: string) => (): SearchResult => {
return {
entity: chartEntity(tool),
matchedFields: [],
__typename: 'SearchResult',
};
};
export const chartBrowsePaths: EntityBrowsePath[] = [{ name: 'superset', paths: [], count: 6 }];
const generateSearchResults = (): SearchResult[] => {
return chartBrowsePaths.flatMap(({ name, count = 0 }) => {
return generateData<SearchResult>({ generator: searchResult(name), count });
});
};
const searchResults = generateSearchResults();
export const chartSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [
{ field: 'access', aggregations: [], __typename: 'FacetMetadata' },
{
field: 'type',
aggregations: [
{ value: 'TABLE', count: 1, __typename: 'AggregationMetadata' },
{ value: 'BAR', count: 3, __typename: 'AggregationMetadata' },
{ value: 'PIE', count: 1, __typename: 'AggregationMetadata' },
{ value: 'LINE', count: 1, __typename: 'AggregationMetadata' },
],
__typename: 'FacetMetadata',
},
{
field: 'tool',
aggregations: [{ value: 'superset', count: 6, __typename: 'AggregationMetadata' }],
__typename: 'FacetMetadata',
},
{ field: 'queryType', aggregations: [], __typename: 'FacetMetadata' },
],
__typename: 'SearchResults',
};
export const filterChartByTool = (tool: string): Chart[] => {
return searchResults
.filter((r) => {
return (r.entity as Chart).tool === tool;
})
.map((r) => r.entity as Chart);
};
export const findChartByURN = (urn: string): Chart => {
return searchResults.find((r) => {
return (r.entity as Chart).urn === urn;
})?.entity as Chart;
};
export const filterChartByPath = (path: string[]): Chart[] => {
return filterEntityByPath({ term: path.slice(-2).join('.'), searchResults }) as Chart[];
};

View File

@ -0,0 +1,57 @@
import { Dashboard, SearchResult, SearchResults } from '../../../types.generated';
import { EntityBrowsePath } from '../../types';
import { filterEntityByPath } from '../browsePathHelper';
import { dashboardEntity } from '../entity/dashboardEntity';
import { generateData } from './dataGenerator';
const searchResult = (tool: string) => (): SearchResult => {
return {
entity: dashboardEntity(tool),
matchedFields: [],
__typename: 'SearchResult',
};
};
export const dashboardBrowsePaths: EntityBrowsePath[] = [{ name: 'superset', paths: [], count: 3 }];
const generateSearchResults = (): SearchResult[] => {
return dashboardBrowsePaths.flatMap(({ name, count = 0 }) => {
return generateData<SearchResult>({ generator: searchResult(name), count });
});
};
const searchResults = generateSearchResults();
export const dashboardSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [
{
field: 'tool',
aggregations: [{ value: 'superset', count: 1, __typename: 'AggregationMetadata' }],
__typename: 'FacetMetadata',
},
{ field: 'access', aggregations: [], __typename: 'FacetMetadata' },
],
__typename: 'SearchResults',
};
export const filterDashboardByTool = (tool: string): Dashboard[] => {
return searchResults
.filter((r) => {
return (r.entity as Dashboard).tool === tool;
})
.map((r) => r.entity as Dashboard);
};
export const findDashboardByURN = (urn: string): Dashboard => {
return searchResults.find((r) => {
return (r.entity as Dashboard).urn === urn;
})?.entity as Dashboard;
};
export const filterDashboardByPath = (path: string[]): Dashboard[] => {
return filterEntityByPath({ term: path.slice(-2).join('.'), searchResults }) as Dashboard[];
};

View File

@ -0,0 +1,72 @@
import { DataFlow, SearchResult, SearchResults } from '../../../types.generated';
import { EntityBrowsePath } from '../../types';
import { filterEntityByPath } from '../browsePathHelper';
import { dataFlowEntity, DataFlowEntityArg } from '../entity/dataFlowEntity';
import { generateData } from './dataGenerator';
type SearchResultArg = DataFlowEntityArg;
const searchResult =
({ orchestrator, cluster }: SearchResultArg) =>
(): SearchResult => {
return {
entity: dataFlowEntity({ orchestrator, cluster }),
matchedFields: [],
__typename: 'SearchResult',
};
};
export const dataFlowBrowsePaths: EntityBrowsePath[] = [
{
name: 'airflow',
paths: [
{
name: 'prod',
paths: [],
count: 4,
},
{
name: 'dev',
paths: [],
count: 1,
},
],
},
];
const generateSearchResults = (): SearchResult[] => {
return dataFlowBrowsePaths.flatMap(({ name: orchestrator, paths }) => {
return paths.flatMap(({ name: cluster, count = 0 }) => {
return generateData<SearchResult>({ generator: searchResult({ orchestrator, cluster }), count });
});
});
};
const searchResults = generateSearchResults();
export const dataFlowSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [],
__typename: 'SearchResults',
};
export const filterDataFlowByOrchestrator = (orchestrator: string): DataFlow[] => {
return searchResults
.filter((r) => {
return (r.entity as DataFlow).orchestrator === orchestrator;
})
.map((r) => r.entity as DataFlow);
};
export const findDataFlowByURN = (urn: string): DataFlow => {
return searchResults.find((r) => {
return (r.entity as DataFlow).urn === urn;
})?.entity as DataFlow;
};
export const filterDataFlowByPath = (path: string[]): DataFlow[] => {
return filterEntityByPath({ term: path.slice(-2).join(',[\\s\\S]+,'), searchResults }) as DataFlow[];
};

View File

@ -0,0 +1,16 @@
import { AnyRecord } from '../../types';
type GenerateDataArg<T = AnyRecord> = {
generator(): T;
count: number;
};
export const times = (count: number) => {
return Array.from(new Array(count));
};
export const generateData = <T = AnyRecord>({ generator, count }: GenerateDataArg<T>): T[] => {
return times(count).map(() => {
return generator();
});
};

View File

@ -0,0 +1,32 @@
import { DataJob, SearchResult, SearchResults } from '../../../types.generated';
import { dataJobEntity } from '../entity/dataJobEntity';
import { generateData } from './dataGenerator';
const searchResult = (): SearchResult => {
return {
entity: dataJobEntity(),
matchedFields: [],
__typename: 'SearchResult',
};
};
const generateSearchResults = (): SearchResult[] => {
return generateData<SearchResult>({ generator: searchResult, count: 2 });
};
const searchResults = generateSearchResults();
export const dataJobSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [],
__typename: 'SearchResults',
};
export const findDataJobByURN = (urn: string): DataJob => {
return searchResults.find((r) => {
return (r.entity as DataJob).urn === urn;
})?.entity as DataJob;
};

View File

@ -0,0 +1,280 @@
import { generateData } from './dataGenerator';
import { datasetEntity, DatasetEntityArg } from '../entity/datasetEntity';
import { Dataset, FabricType, SearchResult, SearchResults } from '../../../types.generated';
import { EntityBrowsePath, StringNumber } from '../../types';
import { filterEntityByPath, toFlatPaths } from '../browsePathHelper';
type SearchResultArg = DatasetEntityArg;
const searchResult =
({ platform, origin, path }: SearchResultArg) =>
(): SearchResult => {
return {
entity: datasetEntity({ platform, origin, path }),
matchedFields: [],
__typename: 'SearchResult',
};
};
export const datasetBrowsePaths: EntityBrowsePath[] = [
{
name: FabricType.Prod.toLowerCase(),
paths: [
{
name: 'kafka',
paths: [
{
name: 'australia',
paths: [
{
name: 'queensland',
paths: [
{
name: 'brisbane',
paths: [
{
name: 'queensland-brisbane-sunnybank-vaccine-test',
paths: [
{
name: 'topic-queensland-brisbane-sunnybank-vaccine-test-jan',
paths: [],
count: 2,
},
{
name: 'topic-queensland-brisbane-sunnybank-vaccine-test-feb',
paths: [],
count: 1,
},
],
},
{
name: 'topic-queensland-brisbane-carindale-vaccine-test-jan',
paths: [],
count: 2,
},
],
},
{
name: 'topic-queensland-sunshine-coast-vaccine-test-feb',
paths: [],
count: 1,
},
],
},
{
name: 'victoria',
paths: [
{
name: 'topic-victoria-vaccine-test-mar',
paths: [],
count: 2,
},
],
},
],
},
{
name: 'topic-papua-new-guinea-digital-transformation',
paths: [],
count: 3,
},
],
},
{
name: 's3',
paths: [
{
name: 'datahub-demo-backup',
paths: [
{
name: 'demo',
paths: [],
count: 3,
},
],
},
{
name: 'datahubproject-demo-pipelines',
paths: [
{
name: 'entity_aspect_splits',
paths: [],
count: 21,
},
],
},
],
},
{
name: 'snowflake',
paths: [
{
name: 'mydb',
paths: [
{
name: 'schema',
paths: [],
count: 3,
},
],
},
{
name: 'demo_pipeline',
paths: [
{
name: 'public',
paths: [],
count: 2,
},
],
},
],
},
{
name: 'bigquery',
paths: [
{
name: 'bigquery-pubic-data',
paths: [
{
name: 'covid19-ecdc',
paths: [],
count: 1,
},
{
name: 'covid19-open-data',
paths: [],
count: 24,
},
],
},
],
},
],
},
{
name: FabricType.Dev.toLowerCase(),
paths: [
{
name: 'kafka',
paths: [
{
name: 'topic-mysql-customer-schema',
paths: [],
count: 1,
},
{
name: 'rnd',
paths: [
{
name: 'topic-mysql-marketing-ml-schema',
paths: [],
count: 3,
},
{
name: 'topic-mysql-sales-ml-schema',
paths: [],
count: 2,
},
],
},
],
},
],
},
];
const generateSearchResults = (): SearchResult[] => {
return datasetBrowsePaths.flatMap(({ name: origin, paths }) => {
return paths.flatMap(({ name: platform, paths: childPaths }) => {
const flatPaths: StringNumber[][] = [];
const parentPaths: string[] = [];
toFlatPaths({ flatPaths, paths: childPaths, parentPaths });
return flatPaths.flatMap((fp) => {
const count = fp.pop() as number;
const path = fp.join('.');
return generateData<SearchResult>({
generator: searchResult({ platform, origin: origin.toUpperCase() as FabricType, path }),
count,
});
});
});
});
};
const searchResults = generateSearchResults();
export const datasetSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [
{
field: 'platform',
aggregations: [
{
value: 's3',
count: 22,
__typename: 'AggregationMetadata',
},
{
value: 'snowflake',
count: 69,
__typename: 'AggregationMetadata',
},
{
value: 'bigquery',
count: 104,
__typename: 'AggregationMetadata',
},
{
value: 'kafka',
count: 7,
__typename: 'AggregationMetadata',
},
],
__typename: 'FacetMetadata',
},
{
field: 'origin',
aggregations: [
{
value: 'prod',
count: 202,
__typename: 'AggregationMetadata',
},
],
__typename: 'FacetMetadata',
},
],
__typename: 'SearchResults',
};
export const filterDatasetByPlatform = (platform: string): Dataset[] => {
return searchResults
.filter((r) => {
return (r.entity as Dataset).platform.name === platform;
})
.map((r) => r.entity as Dataset);
};
export const filterDatasetByName = (name: string): Dataset[] => {
return searchResults
.filter((r) => {
return (r.entity as Dataset).name.indexOf(name) >= 0;
})
.map((r) => r.entity as Dataset);
};
export const findDatasetByURN = (urn: string): Dataset => {
return searchResults.find((r) => {
return (r.entity as Dataset).urn === urn;
})?.entity as Dataset;
};
export const filterDatasetByPath = (path: string[]): Dataset[] => {
return filterEntityByPath({ term: path.slice(-2).join('.'), searchResults }) as Dataset[];
};

View File

@ -0,0 +1,6 @@
export { datasetSearchResult } from './datasetSearchResult';
export { dashboardSearchResult } from './dashboardSearchResult';
export { dataFlowSearchResult } from './dataFlowSearchResult';
export { dataJobSearchResult } from './dataJobSearchResult';
export { chartSearchResult } from './chartSearchResult';
export { userSearchResult } from './userSearchResult';

View File

@ -0,0 +1,54 @@
/* eslint-disable prefer-object-spread */
import { CorpUser, SearchResult, SearchResults } from '../../../types.generated';
import { userEntity, UserEntityArg } from '../entity/userEntity';
import { generateData } from './dataGenerator';
// login with one of these usernames
const usernames = ['kafka', 'looker', 'datahub'];
type SearchResultArg = UserEntityArg;
const searchResult = (option?: SearchResultArg) => (): SearchResult => {
return {
entity: userEntity(option),
matchedFields: [],
__typename: 'SearchResult',
};
};
const generateSearchResults = (): SearchResult[] => {
const loginUsers = usernames.map((username) => {
return searchResult({ username })();
});
return [...loginUsers, ...generateData<SearchResult>({ generator: searchResult(), count: 25 })];
};
const searchResults = generateSearchResults();
export const userSearchResult: SearchResults = {
start: 0,
count: 0,
total: 0,
searchResults,
facets: [],
__typename: 'SearchResults',
};
export const findUserByUsername = (username: string): CorpUser => {
const result = searchResults.find((r) => {
return (r.entity as CorpUser).username === username;
});
return Object.assign({}, result?.entity as CorpUser);
};
export const getUsers = (): CorpUser[] => {
return searchResults.map((r) => Object.assign({}, r.entity as CorpUser));
};
export const findUserByURN = (urn: string | null): CorpUser => {
return searchResults.find((r) => {
return (r.entity as CorpUser).urn === urn;
})?.entity as CorpUser;
};

View File

@ -0,0 +1,49 @@
import * as faker from 'faker';
import { EntityType, Ownership, OwnershipType, Tag, TagUpdate } from '../../types.generated';
import { getActor } from '../helper';
import { findUserByURN } from './searchResult/userSearchResult';
export const tagDb: Tag[] = [];
export const generateTag = (ownership?: Ownership): Tag => {
const name = `${faker.company.bsNoun()}`;
const description = `${faker.commerce.productDescription()}`;
const tag: Tag = {
urn: `urn:li:tag:${name}`,
name,
description,
type: EntityType.Tag,
ownership,
__typename: 'Tag',
};
tagDb.push(tag);
return tag;
};
export const createTag = ({ name, urn, description }: TagUpdate): Tag => {
const user = findUserByURN(getActor());
const tag: Tag = {
urn,
name,
description,
type: EntityType.Tag,
ownership: {
owners: [
{
owner: user,
type: OwnershipType.Dataowner,
__typename: 'Owner',
},
],
lastModified: { time: Date.now(), __typename: 'AuditStamp' },
__typename: 'Ownership',
},
__typename: 'Tag',
};
tagDb.push(tag);
return tag;
};

View File

@ -0,0 +1,21 @@
import { getUsers } from './searchResult/userSearchResult';
const createCorpUserSchema = ({ server, user }) => {
const { info, editableInfo } = user;
// eslint-disable-next-line no-param-reassign
delete user.info;
// eslint-disable-next-line no-param-reassign
delete user.editableInfo;
const userSchema = server.create('CorpUser', user);
userSchema.createInfo(info);
userSchema.createEditableInfo(editableInfo);
};
export const createLoginUsers = (server) => {
const users = getUsers();
users.forEach((user) => {
createCorpUserSchema({ server, user });
});
};

View File

@ -0,0 +1,19 @@
import { EntityType } from '../types.generated';
/**
* Common helpers
*/
export const getActor = (): string | null => {
const cookie = new URLSearchParams(document.cookie.replaceAll('; ', '&'));
return cookie.get('actor');
};
export const toLowerCaseEntityType = (type: EntityType): string => {
return type.toLowerCase().replace(/[_]/g, '');
};
export const toTitleCase = (str: string): string => {
// eslint-disable-next-line no-useless-escape
return `${str.charAt(0).toUpperCase()}${str.substr(1)}`.replace(/[\-_]/g, '');
};

View File

@ -0,0 +1,105 @@
import {
Chart,
Dashboard,
DataFlow,
DataJob,
Dataset,
Entity,
EntityType,
GlobalTags,
GlobalTagsUpdate,
InstitutionalMemory,
InstitutionalMemoryMetadata,
InstitutionalMemoryUpdate,
Owner,
OwnerUpdate,
TagAssociation,
} from '../types.generated';
import { findUserByURN } from './fixtures/searchResult/userSearchResult';
import { tagDb } from './fixtures/tag';
import { getActor } from './helper';
type UpdateEntityOwnersArg = {
entity?: Entity;
owners?: OwnerUpdate[];
};
export const updateEntityOwners = ({ entity, owners }: UpdateEntityOwnersArg) => {
const updateOwners = owners
?.map((o) => {
const user = findUserByURN(o.owner);
return user
? {
owner: user,
type: o.type,
__typename: 'Owner',
}
: null;
})
.filter(Boolean) as Owner[];
const dataEntity = entity as Dataset | Chart | Dashboard | DataFlow | DataJob;
if (dataEntity?.ownership?.owners) {
// eslint-disable-next-line no-param-reassign
dataEntity.ownership.owners = updateOwners;
}
};
type UpdateEntityTagArg = {
entity?: Entity;
globalTags: GlobalTagsUpdate;
};
export const updateEntityTag = ({ entity, globalTags }: UpdateEntityTagArg) => {
const tagAssociations = globalTags.tags
?.map((t) => {
const tag = tagDb.find((ti) => {
return ti.urn === t.tag.urn;
});
return tag
? {
tag,
__typename: 'TagAssociation',
}
: null;
})
.filter(Boolean) as TagAssociation[];
const baseTags: TagAssociation[] = [];
const baseGlobalTags: GlobalTags = {
__typename: 'GlobalTags',
tags: baseTags,
};
const dataEntity = entity as Dataset | Chart | Dashboard | DataFlow | DataJob;
dataEntity.globalTags = dataEntity.globalTags || baseGlobalTags;
if (dataEntity.globalTags.tags) {
dataEntity.globalTags.tags = tagAssociations;
}
};
type UpdateEntityLinkArg = {
entity: Dataset;
institutionalMemory: InstitutionalMemoryUpdate;
};
export const updateEntityLink = ({ entity, institutionalMemory }: UpdateEntityLinkArg) => {
const dataEntity = entity;
const baseElements: InstitutionalMemoryMetadata[] = [];
const baseInstitutionalMemory: InstitutionalMemory = {
elements: baseElements,
__typename: 'InstitutionalMemory',
};
dataEntity.institutionalMemory = dataEntity.institutionalMemory || baseInstitutionalMemory;
dataEntity.institutionalMemory.elements = institutionalMemory.elements.map((e) => {
const link: InstitutionalMemoryMetadata = {
__typename: 'InstitutionalMemoryMetadata',
url: e.url,
description: e.description as string,
author: { urn: e.author, username: '', type: EntityType.CorpUser },
created: { time: Date.now(), actor: getActor(), __typename: 'AuditStamp' },
};
return link;
});
};

View File

@ -0,0 +1,152 @@
import {
AutoCompleteAllResults,
AutoCompleteInput,
AutoCompleteResultForEntity,
Chart,
CorpUser,
Dashboard,
DataFlow,
DataJob,
Dataset,
EntityType,
Maybe,
} from '../../types.generated';
import * as fixtures from '../fixtures';
import { tagDb } from '../fixtures/tag';
type GetAutoCompleteAllResults = {
data: {
autoCompleteForAll: AutoCompleteAllResults;
};
};
const findSuggestions = ({ query, type }: AutoCompleteInput): AutoCompleteResultForEntity[] => {
const q = query.toLowerCase().trim();
if (type === EntityType.Tag) {
const results = q
? tagDb.filter((t) => {
return t.name.indexOf(q) >= 0;
})
: [];
return [
{
type: EntityType.Tag,
suggestions: results.map((r) => {
return r.name;
}),
__typename: 'AutoCompleteResultForEntity',
},
];
}
const filterSearchResults = (): AutoCompleteResultForEntity[] => {
const datasetResults = fixtures.datasetSearchResult.searchResults.filter((r) => {
return ((r.entity as Dataset).name?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const datasetQueryResults: Maybe<AutoCompleteResultForEntity> = datasetResults.length
? {
type: EntityType.Dataset,
suggestions: datasetResults.map((r) => {
return (r.entity as Dataset).name;
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
const dashboardResults = fixtures.dashboardSearchResult.searchResults.filter((r) => {
return ((r.entity as Dashboard).info?.name?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const dashboardQueryResults: Maybe<AutoCompleteResultForEntity> = dashboardResults.length
? {
type: EntityType.Dashboard,
suggestions: dashboardResults.map((r) => {
return (r.entity as Dashboard).info?.name || '';
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
const chartResults = fixtures.chartSearchResult.searchResults.filter((r) => {
return ((r.entity as Chart).info?.name?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const chartQueryResults: Maybe<AutoCompleteResultForEntity> = chartResults.length
? {
type: EntityType.Chart,
suggestions: chartResults.map((r) => {
return (r.entity as Chart).info?.name || '';
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
const dataFlowResults = fixtures.dataFlowSearchResult.searchResults.filter((r) => {
return ((r.entity as DataFlow).info?.name?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const dataFlowQueryResults: Maybe<AutoCompleteResultForEntity> = dataFlowResults.length
? {
type: EntityType.DataFlow,
suggestions: dataFlowResults.map((r) => {
return (r.entity as DataFlow).info?.name || '';
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
const dataJobResults = fixtures.dataJobSearchResult.searchResults.filter((r) => {
return ((r.entity as DataJob).info?.name?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const dataJobQueryResults: Maybe<AutoCompleteResultForEntity> = dataJobResults.length
? {
type: EntityType.DataJob,
suggestions: dataJobResults.map((r) => {
return (r.entity as DataJob).info?.name || '';
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
const userResults = fixtures.userSearchResult.searchResults.filter((r) => {
return ((r.entity as CorpUser).info?.fullName?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
});
const userQueryResults: Maybe<AutoCompleteResultForEntity> = userResults.length
? {
type: EntityType.CorpUser,
suggestions: userResults.map((r) => {
return (r.entity as CorpUser).info?.fullName || '';
}),
__typename: 'AutoCompleteResultForEntity',
}
: null;
return [
datasetQueryResults,
dashboardQueryResults,
chartQueryResults,
dataFlowQueryResults,
dataJobQueryResults,
userQueryResults,
].filter(Boolean) as AutoCompleteResultForEntity[];
};
return q ? filterSearchResults() : [];
};
export const getAutoCompleteAllResultsResolver = {
getAutoCompleteAllResults({ variables: { input } }): GetAutoCompleteAllResults {
const { query }: AutoCompleteInput = input;
const suggestions = findSuggestions(input);
return {
data: {
autoCompleteForAll: {
query,
suggestions: suggestions.filter((s, i) => {
return suggestions.indexOf(s) === i;
}),
__typename: 'AutoCompleteAllResults',
},
},
};
},
};

View File

@ -0,0 +1,94 @@
import {
AutoCompleteInput,
AutoCompleteResults,
Chart,
CorpUser,
Dashboard,
DataFlow,
DataJob,
Dataset,
EntityType,
} from '../../types.generated';
import * as fixtures from '../fixtures';
import { tagDb } from '../fixtures/tag';
type GetAutoCompleteResults = {
data: {
autoComplete: AutoCompleteResults;
};
};
const findSuggestions = ({ query, type, field }: AutoCompleteInput): string[] => {
const q = query.toLowerCase().trim();
if (type === EntityType.Tag) {
const results = q
? tagDb.filter((t) => {
return t.name.indexOf(q) >= 0;
})
: [];
return results.map((r) => {
return r.name;
});
}
const filterSearchResults = () => {
if (field === 'ldap') {
return fixtures.userSearchResult.searchResults.filter((r) => {
return ((r.entity as CorpUser).username?.indexOf(q) ?? -1) >= 0;
});
}
return [
...fixtures.datasetSearchResult.searchResults.filter((r) => {
return ((r.entity as Dataset).name?.indexOf(q) ?? -1) >= 0;
}),
...fixtures.dashboardSearchResult.searchResults.filter((r) => {
return ((r.entity as Dashboard).info?.name?.indexOf(q) ?? -1) >= 0;
}),
...fixtures.chartSearchResult.searchResults.filter((r) => {
return ((r.entity as Chart).info?.name?.indexOf(q) ?? -1) >= 0;
}),
...fixtures.dataFlowSearchResult.searchResults.filter((r) => {
return ((r.entity as DataFlow).info?.name?.indexOf(q) ?? -1) >= 0;
}),
...fixtures.dataJobSearchResult.searchResults.filter((r) => {
return ((r.entity as DataJob).info?.name?.indexOf(q) ?? -1) >= 0;
}),
...fixtures.userSearchResult.searchResults.filter((r) => {
return ((r.entity as CorpUser).info?.fullName?.toLowerCase()?.indexOf(q) ?? -1) >= 0;
}),
];
};
const results = q ? filterSearchResults() : [];
return results
.map((r) => {
return field === 'ldap'
? (r.entity as CorpUser)?.username
: (r.entity as Dataset)?.name ||
(r.entity as Dashboard | Chart | DataFlow | DataJob)?.info?.name ||
(r.entity as CorpUser)?.info?.fullName ||
'';
})
.filter(Boolean);
};
export const getAutoCompleteResultsResolver = {
getAutoCompleteResults({ variables: { input } }): GetAutoCompleteResults {
const { query }: AutoCompleteInput = input;
const suggestions = findSuggestions(input);
return {
data: {
autoComplete: {
query,
suggestions: suggestions.filter((s, i) => {
return suggestions.indexOf(s) === i;
}),
__typename: 'AutoCompleteResults',
},
},
};
},
};

View File

@ -0,0 +1,44 @@
import { BrowsePath, BrowsePathsInput, EntityType } from '../../types.generated';
const paths = {
[EntityType.Dataset](urn) {
const result = urn.replace('urn:li:dataset:(urn:li:dataPlatform:', '').replace(')', '').split(',');
return [result[result.length - 1].toLowerCase(), result[0], ...result[1].split('.')];
},
[EntityType.Dashboard](urn) {
return urn.replace('urn:li:dashboard:(', '').replace(')', '').split(',');
},
[EntityType.Chart](urn) {
return urn.replace('urn:li:chart:(', '').replace(')', '').split(',');
},
[EntityType.DataFlow](urn) {
const result = urn.replace('urn:li:dataFlow:(', '').replace(')', '').split(',');
return [result[0], result[result.length - 1].toLowerCase(), result[1]];
},
[EntityType.DataJob]() {
return [];
},
};
type GetBrowsePaths = {
data: {
browsePaths: BrowsePath[];
};
};
export const getBrowsePathsResolver = {
getBrowsePaths({ variables: { input } }): GetBrowsePaths {
const { urn, type }: BrowsePathsInput = input;
return {
data: {
browsePaths: [
{
path: paths[type](urn),
__typename: 'BrowsePath',
},
],
},
};
},
};

View File

@ -0,0 +1,27 @@
import * as fixtures from '../fixtures';
import { BrowseInput } from '../../types.generated';
import { EntityBrowseFn, GetBrowseResults } from '../types';
import { toLowerCaseEntityType, toTitleCase } from '../helper';
const toPathTitle = (paths: string[]): string => {
return paths?.map((p) => toTitleCase(p)).join('');
};
export const getBrowseResultsResolver = {
getBrowseResults({ variables: { input } }): GetBrowseResults {
const { type, path = [], start = 0, count = 0 }: BrowseInput = input;
const startValue = start as number;
const countValue = count as number;
const paths = path as string[];
const entityType = toLowerCaseEntityType(type);
const pathTitle = toPathTitle(paths);
const result: GetBrowseResults | EntityBrowseFn =
fixtures[`${entityType}BrowseResult`][`${entityType}Browse${pathTitle}`];
if (typeof result === 'function') {
return result({ start: startValue, count: countValue, path: paths });
}
return result;
},
};

View File

@ -0,0 +1,39 @@
import { Chart } from '../../types.generated';
import { findChartByURN } from '../fixtures/searchResult/chartSearchResult';
type GetChart = {
data: {
chart: Chart;
};
};
export const getChartResolver = {
getChart({ variables: { urn } }): GetChart {
const chart = findChartByURN(urn) as Chart;
return {
data: {
chart: Object.assign(chart, {
info: {
...chart.info,
inputs: [],
customProperties: [],
lastRefreshed: null,
created: {
time: 1619160920,
__typename: 'AuditStamp',
},
},
query: null,
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,36 @@
import { Dashboard } from '../../types.generated';
import { findDashboardByURN } from '../fixtures/searchResult/dashboardSearchResult';
type GetDashboard = {
data: { dashboard: Dashboard };
};
export const getDashboardResolver = {
getDashboard({ variables: { urn } }): GetDashboard {
const dashboard = findDashboardByURN(urn) as Dashboard;
return {
data: {
dashboard: Object.assign(dashboard, {
info: {
...dashboard.info,
charts: [],
customProperties: [],
lastRefreshed: null,
created: {
time: 1619160920,
__typename: 'AuditStamp',
},
},
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,24 @@
import { DataFlow } from '../../types.generated';
import { findDataFlowByURN } from '../fixtures/searchResult/dataFlowSearchResult';
type GetDataFlow = {
data: { dataFlow: DataFlow };
};
export const getDataFlowResolver = {
getDataFlow({ variables: { urn } }): GetDataFlow {
const dataFlow = findDataFlowByURN(urn) as DataFlow;
return {
data: {
dataFlow: Object.assign(dataFlow, {
info: {
...dataFlow.info,
externalUrl: 'https://airflow.demo.datahubproject.io/tree?dag_id=datahub_analytics_refresh',
inputs: [],
customProperties: [],
},
}),
},
};
},
};

View File

@ -0,0 +1,32 @@
import { DataJob } from '../../types.generated';
import { findDataJobByURN } from '../fixtures/searchResult/dataJobSearchResult';
type GetJobFlow = {
data: { dataJob: DataJob };
};
export const getDataJobResolver = {
getDataJob({ variables: { urn } }): GetJobFlow {
const dataJob = findDataJobByURN(urn) as DataJob;
return {
data: {
dataJob: Object.assign(dataJob, {
info: {
...dataJob.info,
externalUrl: 'https://airflow.demo.datahubproject.io/tree?dag_id=datahub_analytics_refresh',
inputs: [],
customProperties: [],
},
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,43 @@
import { Dataset, InstitutionalMemory, InstitutionalMemoryMetadata } from '../../types.generated';
import { findDatasetByURN } from '../fixtures/searchResult/datasetSearchResult';
type GetDataset = {
data: { dataset: Dataset };
};
export const getDatasetResolver = {
getDataset({ variables: { urn } }): GetDataset {
const dataset = findDatasetByURN(urn);
if (!dataset.institutionalMemory) {
const baseElements: InstitutionalMemoryMetadata[] = [];
const baseInstitutionalMemory: InstitutionalMemory = {
elements: baseElements,
__typename: 'InstitutionalMemory',
};
dataset.institutionalMemory = baseInstitutionalMemory;
}
return {
data: {
dataset: Object.assign(dataset, {
schema: null,
editableProperties: null,
editableSchemaMetadata: null,
deprecation: null,
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
glossaryTerms: null,
institutionalMemory: null,
usageStats: null,
}),
},
};
},
};

View File

@ -0,0 +1,114 @@
import * as fixtures from '../fixtures';
import {
Chart,
CorpUser,
Dashboard,
DataFlow,
DataJob,
Dataset,
EntityType,
SearchInput,
SearchResult,
SearchResults,
} from '../../types.generated';
const entitySearchResults = {
[EntityType.Dataset]: fixtures.datasetSearchResult,
[EntityType.Dashboard]: fixtures.dashboardSearchResult,
[EntityType.Chart]: fixtures.chartSearchResult,
[EntityType.CorpUser]: fixtures.userSearchResult,
[EntityType.DataFlow]: fixtures.dataFlowSearchResult,
[EntityType.DataJob]: fixtures.dataJobSearchResult,
};
type FindByQueryArg = {
query: string;
searchResults: SearchResult[];
start: number;
count: number;
};
type QueryResult = {
results: SearchResult[];
total: number;
};
const findByQuery = ({ query, searchResults, start, count }: FindByQueryArg): QueryResult => {
if (query === '*') {
const results = searchResults.slice(start, start + count);
return { results, total: results.length };
}
if (query.indexOf('owners:') >= 0) {
const [, ownerQuery] = query.split(':');
const results = searchResults.filter((r) => {
return (r.entity as Dataset | Dashboard | Chart | DataFlow | DataJob).ownership?.owners?.filter((o) => {
return (o.owner as CorpUser).username === ownerQuery;
}).length;
});
return { results, total: results.length };
}
if (query.indexOf('tags:') >= 0) {
const [, tagQuery] = query.split(':');
const results = searchResults.filter((r) => {
return (r.entity as Dataset | Dashboard | Chart | DataFlow | DataJob).globalTags?.tags?.filter((t) => {
return t.tag.name === tagQuery;
}).length;
});
return { results, total: results.length };
}
const results = [
...searchResults.filter((r) => {
return ((r.entity as Dataset).name?.indexOf(query) ?? -1) >= 0;
}),
...searchResults.filter((r) => {
return ((r.entity as Dashboard | Chart | DataFlow | DataJob).info?.name?.indexOf(query) ?? -1) >= 0;
}),
...searchResults.filter((r) => {
return ((r.entity as CorpUser).info?.fullName?.toLowerCase()?.indexOf(query) ?? -1) >= 0;
}),
];
return { results, total: results.length };
};
type GetSearchResults = {
data: {
search: SearchResults;
};
} | null;
export const getSearchResultsResolver = {
getSearchResults({ variables: { input } }): GetSearchResults {
const { type, start = 0, count = 10, query }: SearchInput = input;
const startValue = start as number;
const countValue = count as number;
const entitySearchResult: SearchResults = entitySearchResults[type];
const { results, total } = findByQuery({
query: query.toLowerCase(),
searchResults: entitySearchResult.searchResults,
start: startValue,
count: countValue,
});
return entitySearchResult
? {
data: {
search: {
start: startValue,
count: results.length,
total,
searchResults: results,
facets: entitySearchResult.facets,
__typename: entitySearchResult.__typename,
},
},
}
: null;
},
};

View File

@ -0,0 +1,24 @@
import { Tag } from '../../types.generated';
import { tagDb } from '../fixtures/tag';
type GetTag = {
data: { tag: Tag | undefined };
};
export const getTagResolver = {
getTag({ variables: { urn } }): GetTag {
const tag = tagDb.find((t) => {
return t.urn === urn;
});
if (tag && !tag?.ownership) {
tag.ownership = null;
}
return {
data: {
tag,
},
};
},
};

View File

@ -0,0 +1,44 @@
import { getBrowseResultsResolver } from './getBrowseResultsResolver';
import { getSearchResultsResolver } from './getSearchResultsResolver';
import { getAutoCompleteAllResultsResolver } from './getAutoCompleteAllResultsResolver';
import { getAutoCompleteResultsResolver } from './getAutoCompleteResultsResolver';
import { getBrowsePathsResolver } from './getBrowsePathsResolver';
import { getDatasetResolver } from './getDatasetResolver';
import { getDashboardResolver } from './getDashboardResolver';
import { getChartResolver } from './getChartResolver';
import { getDataFlowResolver } from './getDataFlowResolver';
import { getDataJobResolver } from './getDataJobResolver';
import { getTagResolver } from './getTagResolver';
import { isAnalyticsEnabledResolver } from './isAnalyticsEnabledResolver';
import { updateDatasetResolver } from './updateDatasetResolver';
import { updateDashboardResolver } from './updateDashboardResolver';
import { updateChartResolver } from './updateChartResolver';
import { updateDataFlowResolver } from './updateDataFlowResolver';
import { updateDataJobResolver } from './updateDataJobResolver';
import { updateTagResolver } from './updateTagResolver';
const resolver = {
...getSearchResultsResolver,
...getBrowseResultsResolver,
...getAutoCompleteAllResultsResolver,
...getAutoCompleteResultsResolver,
...getBrowsePathsResolver,
...getDatasetResolver,
...getDashboardResolver,
...getChartResolver,
...getDataFlowResolver,
...getDataJobResolver,
...getTagResolver,
...isAnalyticsEnabledResolver,
...updateDatasetResolver,
...updateDashboardResolver,
...updateChartResolver,
...updateDataFlowResolver,
...updateDataJobResolver,
...updateTagResolver,
};
export const resolveRequest = (schema, request) => {
const { operationName, variables } = JSON.parse(request.requestBody);
return resolver[operationName] && resolver[operationName]({ schema, variables });
};

View File

@ -0,0 +1,5 @@
export const isAnalyticsEnabledResolver = {
isAnalyticsEnabled(): boolean {
return false;
},
};

View File

@ -0,0 +1,46 @@
import { Chart, ChartUpdateInput } from '../../types.generated';
import { findChartByURN } from '../fixtures/searchResult/chartSearchResult';
import { updateEntityOwners, updateEntityTag } from '../mutationHelper';
type UpdateChart = {
data: { updateChart: Chart };
};
export const updateChartResolver = {
updateChart({ variables: { input } }): UpdateChart {
const { globalTags, urn, ownership }: ChartUpdateInput = input;
const chart = findChartByURN(urn);
if (ownership) {
updateEntityOwners({ entity: chart, owners: ownership?.owners });
} else if (globalTags) {
updateEntityTag({ entity: chart, globalTags });
}
return {
data: {
updateChart: Object.assign(chart, {
info: {
...chart.info,
inputs: [],
customProperties: [],
lastRefreshed: null,
created: {
time: 1619160920,
__typename: 'AuditStamp',
},
},
query: null,
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,45 @@
import { Dashboard, DashboardUpdateInput } from '../../types.generated';
import { findDashboardByURN } from '../fixtures/searchResult/dashboardSearchResult';
import { updateEntityOwners, updateEntityTag } from '../mutationHelper';
type UpdateDashboard = {
data: { updateDashboard: Dashboard };
};
export const updateDashboardResolver = {
updateDashboard({ variables: { input } }): UpdateDashboard {
const { urn, ownership, globalTags }: DashboardUpdateInput = input;
const dashboard = findDashboardByURN(urn);
if (ownership) {
updateEntityOwners({ entity: dashboard, owners: ownership?.owners });
} else if (globalTags) {
updateEntityTag({ entity: dashboard, globalTags });
}
return {
data: {
updateDashboard: Object.assign(dashboard, {
info: {
...dashboard.info,
charts: [],
customProperties: [],
lastRefreshed: null,
created: {
time: 1619160920,
__typename: 'AuditStamp',
},
},
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,33 @@
import { DataFlow, DataFlowUpdateInput } from '../../types.generated';
import { findDataFlowByURN } from '../fixtures/searchResult/dataFlowSearchResult';
import { updateEntityOwners, updateEntityTag } from '../mutationHelper';
type UpdateDataFlow = {
data: { updateDataFlow: DataFlow };
};
export const updateDataFlowResolver = {
updateDataFlow({ variables: { input } }): UpdateDataFlow {
const { globalTags, urn, ownership }: DataFlowUpdateInput = input;
const dataFlow = findDataFlowByURN(urn);
if (ownership) {
updateEntityOwners({ entity: dataFlow, owners: ownership?.owners });
} else if (globalTags) {
updateEntityTag({ entity: dataFlow, globalTags });
}
return {
data: {
updateDataFlow: Object.assign(dataFlow, {
info: {
...dataFlow.info,
externalUrl: 'https://airflow.demo.datahubproject.io/tree?dag_id=datahub_analytics_refresh',
inputs: [],
customProperties: [],
},
}),
},
};
},
};

View File

@ -0,0 +1,26 @@
import { DataJob, DataJobUpdateInput } from '../../types.generated';
import { findDataJobByURN } from '../fixtures/searchResult/dataJobSearchResult';
import { updateEntityOwners, updateEntityTag } from '../mutationHelper';
type UpdateDataJob = {
data: { updateDataJob: DataJob };
};
export const updateDataJobResolver = {
updateDataJob({ variables: { input } }): UpdateDataJob {
const { urn, ownership, globalTags }: DataJobUpdateInput = input;
const dataJob = findDataJobByURN(urn);
if (ownership) {
updateEntityOwners({ entity: dataJob, owners: ownership?.owners });
} else if (globalTags) {
updateEntityTag({ entity: dataJob, globalTags });
}
return {
data: {
updateDataJob: dataJob,
},
};
},
};

View File

@ -0,0 +1,40 @@
import { Dataset, DatasetUpdateInput } from '../../types.generated';
import { findDatasetByURN } from '../fixtures/searchResult/datasetSearchResult';
import { updateEntityLink, updateEntityOwners, updateEntityTag } from '../mutationHelper';
type UpdateDataset = {
data: { updateDataset: Dataset };
};
export const updateDatasetResolver = {
updateDataset({ variables: { input } }): UpdateDataset {
const { urn, ownership, globalTags, institutionalMemory }: DatasetUpdateInput = input;
const dataset = findDatasetByURN(urn);
if (ownership) {
updateEntityOwners({ entity: dataset, owners: ownership.owners });
} else if (globalTags) {
updateEntityTag({ entity: dataset, globalTags });
} else if (institutionalMemory) {
updateEntityLink({ entity: dataset, institutionalMemory });
}
return {
data: {
updateDataset: Object.assign(dataset, {
schema: null,
editableSchemaMetadata: null,
deprecation: null,
downstreamLineage: {
entities: [],
__typename: 'DownstreamEntityRelationships',
},
upstreamLineage: {
entities: [],
__typename: 'UpstreamEntityRelationships',
},
}),
},
};
},
};

View File

@ -0,0 +1,18 @@
import { Tag } from '../../types.generated';
import { createTag } from '../fixtures/tag';
type UpdateTag = {
data: { updateTag: Tag | undefined };
};
export const updateTagResolver = {
updateTag({ variables: { input } }): UpdateTag {
const tag: Tag = createTag(input);
return {
data: {
updateTag: tag,
},
};
},
};

View File

@ -0,0 +1,14 @@
import { loader } from 'graphql.macro';
import gql from 'graphql-tag';
import { buildASTSchema } from 'graphql';
const gmsSchema = loader('../../../datahub-graphql-core/src/main/resources/gms.graphql');
const feSchema = loader('../../../datahub-frontend/conf/datahub-frontend.graphql');
const graphQLSchemaAST = gql`
${gmsSchema}
${feSchema}
`;
export const graphQLSchema = buildASTSchema(graphQLSchemaAST);

View File

@ -0,0 +1,83 @@
/* eslint-disable no-restricted-syntax */
import { Model, Response, createServer, belongsTo } from 'miragejs';
import { createGraphQLHandler } from '@miragejs/graphql';
import { graphQLSchema } from './schema';
import { GlobalCfg } from '../conf';
import { resolveRequest } from './resolver';
import { createLoginUsers } from './fixtures/user';
import { findUserByURN } from './fixtures/searchResult/userSearchResult';
export function makeServer(environment = 'development') {
return createServer({
environment,
models: {
CorpUser: Model.extend({
info: belongsTo('CorpUserInfo'),
editableInfo: belongsTo('CorpUserEditableInfo'),
}),
},
seeds(server) {
createLoginUsers(server);
console.log(server.db.dump());
},
routes() {
const graphQLHandler = createGraphQLHandler(graphQLSchema, this.schema);
this.post('/api/v2/graphql', (schema, request) => {
return resolveRequest(schema, request) ?? graphQLHandler(schema, request);
});
this.get('/authenticate', () => new Response(200));
this.post('/logIn', (_schema, request) => {
const payload = JSON.parse(request.requestBody);
const cookieExpiration = new Date(Date.now() + 24 * 3600 * 1000);
const urn = `urn:li:corpuser:${payload.username}`;
const user = findUserByURN(urn);
if (!user) {
return new Response(404);
}
document.cookie = `${
GlobalCfg.CLIENT_AUTH_COOKIE
}=${urn}; domain=localhost; path=/; expires=${cookieExpiration.toUTCString()};`;
return new Response(200);
});
this.post('/track', () => new Response(200));
},
});
}
export function makeServerForCypress() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (window.Cypress) {
const otherDomains = [];
const methods = ['head', 'get', 'put', 'patch', 'post', 'delete'];
createServer({
environment: 'test',
routes() {
for (const domain of ['/*', ...otherDomains]) {
for (const method of methods) {
this[method](`${domain}`, async (_schema, request) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const [status, headers, body] = await window.handleFromCypress(request);
return new Response(status, headers, body);
});
}
}
},
});
}
}

View File

@ -0,0 +1,36 @@
import { BrowseResults } from '../types.generated';
export type DataSchema = {
id?: string | undefined;
attrs: AnyRecord;
modelName: string;
save(): void;
update<K extends never>(key: K, value: AnyRecord[K]): void;
update(changes: Partial<AnyRecord>): void;
destroy(): void;
reload(): void;
};
export type AnyRecord = Record<string, unknown>;
export type EntityBrowsePath = {
name: string;
paths: EntityBrowsePath[];
count?: number;
};
export type StringNumber = string | number;
export type GetBrowseResults = {
data: {
browse: BrowseResults;
};
};
type EntityBrowseFnArg = {
start: number;
count: number;
path: string[];
};
export type EntityBrowseFn = (arg: EntityBrowseFnArg) => GetBrowseResults;

View File

@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import './graphql-mock/createServer';
import App from './App';
import reportWebVitals from './reportWebVitals';

View File

@ -0,0 +1,24 @@
if (process.env.REACT_APP_MOCK === 'true' || process.env.REACT_APP_MOCK === 'cy') {
// no proxy needed, MirageJS will intercept all http requests
module.exports = function () {};
} else {
// create a proxy to the graphql server running in docker container
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/logIn',
createProxyMiddleware({
target: 'http://localhost:9002',
changeOrigin: true,
}),
);
app.use(
'/api/v2/graphql',
createProxyMiddleware({
target: 'http://localhost:9002',
changeOrigin: true,
}),
);
};
}

File diff suppressed because it is too large Load Diff