Skip to content

Commit

Permalink
feat: improved data fetching for execution details page (#131)
Browse files Browse the repository at this point in the history
* refactor: integrate react-query for execution data fetching (#123)

* refactor: using react-query to load top level Execution

* refactor: upgrading react-query and fixing execution termination

* refactor: handle 401s on queries and do auth flow

* refactor: adding conditional refresh for execution status

* refactor: cleanup broken files after context refactor

* chore: docs

* refactor: Remove ExecutionDataCache in favor of react-query (#126)

* refactor: first step of using queries for NE table

* refactor: removing data cache from first layer of NE table

* refactor: removing remaining execution data cache usage

* refactor: rename QueryKey type and remove bug workaround

* refactor: fixing remaining consumers of NEs

* test: adds setup for mock-service-worker (#127)

* test: add msw and basic handlers for a few types

* test: add mock data for a basic workflow execution

* test: fixing/removing tests after adding msw

* test: throw on unexpected requests to msw

* fix: upgrade TS to fix error and cleanup resulting errors

* Migrate from TSLint to ESLint (#128)

* ci: move from tslint->eslint

* fix: addressing eslint errors

* fix: remove passing of unused variable

* ci: remove unnecessary prettier config

* refactor: clean up mock fixtures and re-enable tests for executions (#130)

* test: adding test data for node executions

* test: mocks and refactoring to re-enable NodeExecutionDetails tests

* chore: lint error

* test: getting first test for NE table working again

* test: mocks and a couple of tests for NE table

* refactor: msw handlers to use a backing map and return 404s

* test: more tests for NE table

* test: adding fixture for dynamic external workflow

* test: using mock fixture for sub workflow tests

* test: move remaining mocks to fixtures and fix tests

* test: re-enabling more execution tests

* fix: removing global query handlers for caching entitiesq

* test: re-enable ExecutionNodeViews tests

* fix: typo in import path

* fix: show DataError by default for failed queries

* chore: documentation

* chore: pr feedback

* test: fixing storybook rendering for NE table

* refactor: queries and loading states for (Task)ExecutionDetails

* chore: cleanup unused code

* fix: adds mock support for launch plans

* test: update LoadingSpinner tests

* fix: handle error case for NE children

* chore: remove todo

* fix: typo
  • Loading branch information
schottra authored Jan 6, 2021
1 parent 61a645f commit 928f094
Show file tree
Hide file tree
Showing 140 changed files with 7,539 additions and 5,073 deletions.
8 changes: 8 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
src/generated/
.dist/
dist/
node_modules/
.vscode/
tsd/
package.json
webpack.config.ts
30 changes: 22 additions & 8 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
// We are using eslint with the typescript parser plugin only to validate things
// which are not supported in tslint, such as react hooks
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
jsx: true,
sourceType: 'module'
root: true,
env: {
browser: true,
es6: true,
node: true
},
plugins: ['react-hooks'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react-hooks', 'jest'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:jest/recommended',
'prettier',
'prettier/@typescript-eslint'
],
rules: {
'react-hooks/rules-of-hooks': 'error'
'no-case-declarations': 'warn',
'jest/no-mocks-import': 'off',
'jest/valid-title': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-types': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off'
}
};
23 changes: 15 additions & 8 deletions .storybook/StorybookContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { CssBaseline } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { ThemeProvider } from '@material-ui/styles';
import * as React from 'react';
import { QueryClientProvider } from 'react-query';
import { ErrorBoundary } from '../src/components/common';
import { createQueryClient } from '../src/components/data/queryCache';
import { muiTheme } from '../src/components/Theme/muiTheme';

const useStyles = makeStyles((theme: Theme) => ({
Expand All @@ -12,11 +14,16 @@ const useStyles = makeStyles((theme: Theme) => ({
}
}));

export const StorybookContainer: React.StatelessComponent = ({ children }) => (
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<ErrorBoundary>
<div className={useStyles().container}>{children}</div>
</ErrorBoundary>
</ThemeProvider>
);
export const StorybookContainer: React.FC = ({ children }) => {
const [queryClient] = React.useState(() => createQueryClient());
return (
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<div className={useStyles().container}>{children}</div>
</QueryClientProvider>
</ErrorBoundary>
</ThemeProvider>
);
};
25 changes: 0 additions & 25 deletions .storybook/config.js

This file was deleted.

4 changes: 4 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
stories: ['../src/components/**/*.stories.tsx'],
addons: ['@storybook/addon-knobs']
};
22 changes: 22 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import '../src/protobuf';
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';
import * as React from 'react';
import StoryRouter from 'storybook-react-router';
import { StorybookContainer } from './StorybookContainer';

addDecorator(withKnobs);
addDecorator(StoryRouter());
addDecorator(Story => <StorybookContainer><Story /></StorybookContainer>);

// Storybook executes this module in both bootstap phase (Node)
// and a story's runtime (browser). However, cannot call `setupWorker`
// in Node environment, so need to check if we're in a browser.
if (typeof global.process === 'undefined') {
const { worker } = require('./worker');

// Start the mocking when each story is loaded.
// Repetitive calls to the `.start()` method will check for an existing
// worker and reuse it.
worker.start();
}
241 changes: 241 additions & 0 deletions .storybook/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
/* eslint-disable */
/* tslint:disable */

const INTEGRITY_CHECKSUM = '65d33ca82955e1c5928aed19d1bdf3f9'
const bypassHeaderName = 'x-msw-bypass'

let clients = {}

self.addEventListener('install', function () {
return self.skipWaiting()
})

self.addEventListener('activate', async function (event) {
return self.clients.claim()
})

self.addEventListener('message', async function (event) {
const clientId = event.source.id
const client = await event.currentTarget.clients.get(clientId)
const allClients = await self.clients.matchAll()
const allClientIds = allClients.map((client) => client.id)

switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}

case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}

case 'MOCK_ACTIVATE': {
clients = ensureKeys(allClientIds, clients)
clients[clientId] = true

sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}

case 'MOCK_DEACTIVATE': {
clients = ensureKeys(allClientIds, clients)
clients[clientId] = false
break
}

case 'CLIENT_CLOSED': {
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})

// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}

break
}
}
})

self.addEventListener('fetch', function (event) {
const { clientId, request } = event
const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone)

// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}

// Bypass mocking if the current client isn't present in the internal clients map
// (i.e. has the mocking disabled).
if (!clients[clientId]) {
return
}

// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}

event.respondWith(
new Promise(async (resolve, reject) => {
const client = await event.target.clients.get(clientId)

// Bypass mocking when the request client is not active.
if (!client) {
return resolve(getOriginalResponse())
}

// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const modifiedHeaders = serializeHeaders(requestClone.headers)

// Remove the bypass header to comply with the CORS preflight check
delete modifiedHeaders[bypassHeaderName]

const originalRequest = new Request(requestClone, {
headers: new Headers(modifiedHeaders),
})

return resolve(fetch(originalRequest))
}

const reqHeaders = serializeHeaders(request.headers)
const body = await request.text()

const rawClientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})

const clientMessage = rawClientMessage

switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
setTimeout(
resolve.bind(this, createResponse(clientMessage)),
clientMessage.payload.delay,
)
break
}

case 'MOCK_NOT_FOUND': {
return resolve(getOriginalResponse())
}

case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name

// Rejecting a request Promise emulates a network error.
return reject(networkError)
}

case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body)

console.error(
`\
[MSW] Request handler function for "%s %s" has thrown the following exception:
${parsedBody.errorType}: ${parsedBody.message}
(see more detailed error stack trace in the mocked response body)
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url,
)

return resolve(createResponse(clientMessage))
}
}
}).catch((error) => {
console.error(
'[MSW] Failed to mock a "%s" request to "%s": %s',
request.method,
request.url,
error,
)
}),
)
})

function serializeHeaders(headers) {
const reqHeaders = {}
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value
})
return reqHeaders
}

function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()

channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
reject(event.data.error)
} else {
resolve(event.data)
}
}

client.postMessage(JSON.stringify(message), [channel.port2])
})
}

function createResponse(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
})
}

function ensureKeys(keys, obj) {
return Object.keys(obj).reduce((acc, key) => {
if (keys.includes(key)) {
acc[key] = obj[key]
}

return acc
}, {})
}
19 changes: 19 additions & 0 deletions .storybook/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { setupWorker, SetupWorkerApi } from 'msw';
import { AdminServer, createAdminServer } from '../src/mocks/createAdminServer';
import { basicPythonWorkflow } from '../src/mocks/data/fixtures/basicPythonWorkflow';
import { dynamicExternalSubWorkflow } from '../src/mocks/data/fixtures/dynamicExternalSubworkflow';
import { dynamicPythonNodeExecutionWorkflow } from '../src/mocks/data/fixtures/dynamicPythonWorkflow';
import { oneFailedTaskWorkflow } from '../src/mocks/data/fixtures/oneFailedTaskWorkflow';
import { insertFixture } from '../src/mocks/data/insertFixture';
import { insertDefaultData } from '../src/mocks/insertDefaultData';

const { handlers, server } = createAdminServer();
export type MockServer = SetupWorkerApi & AdminServer;
const worker: MockServer = { ...setupWorker(...handlers), ...server };
insertDefaultData(worker);
insertFixture(worker, basicPythonWorkflow.generate());
insertFixture(worker, dynamicExternalSubWorkflow.generate());
insertFixture(worker, dynamicPythonNodeExecutionWorkflow.generate());
insertFixture(worker, oneFailedTaskWorkflow.generate());

export { worker };
Loading

0 comments on commit 928f094

Please sign in to comment.