diff --git a/wherehows-web/app/actions/browse.js b/wherehows-web/app/actions/browse.js index 7c84543036..6e3c75f4c4 100644 --- a/wherehows-web/app/actions/browse.js +++ b/wherehows-web/app/actions/browse.js @@ -32,11 +32,11 @@ const asyncRequestBrowseData = (page, entity, urls) => try { const thunks = await Promise.all([ - dispatch(lazyRequestPagedFlows({ baseURL: urls.flows, page })), - dispatch(lazyRequestPagedMetrics({ baseURL: urls.metrics, page })), - dispatch(lazyRequestPagedDatasets({ baseURL: urls.datasets, page })) + dispatch(lazyRequestPagedFlows({ baseURL: urls.flows, query: { page } })), + dispatch(lazyRequestPagedMetrics({ baseURL: urls.metrics, query: { page } })), + dispatch(lazyRequestPagedDatasets({ baseURL: urls.datasets, query: { page } })) ]); - const [...actions] = await Promise.all(thunks); + const [...actions] = thunks; /** * Check that none of the actions has an error flag in FSA diff --git a/wherehows-web/app/actions/browse/entity.js b/wherehows-web/app/actions/browse/entity.js index fb4f76d164..742f700393 100644 --- a/wherehows-web/app/actions/browse/entity.js +++ b/wherehows-web/app/actions/browse/entity.js @@ -3,6 +3,8 @@ import { createAction } from 'redux-actions'; import actionSet from 'wherehows-web/actions/action-set'; import { lazyRequestUrnPagedDatasets, lazyRequestDatasetNodes } from 'wherehows-web/actions/datasets'; +import { lazyRequestNamedPagedMetrics, lazyRequestMetricNodes } from 'wherehows-web/actions/metrics'; +import { lazyRequestFlowsNodes, lazyRequestPagedUrnApplicationFlows } from 'wherehows-web/actions/flows'; const { debug } = Ember; @@ -23,29 +25,41 @@ const receiveNodeList = createAction(ActionTypes.RECEIVE_NODE_LIST); * @param {String} listURL * @param {Array} queryParams current list of query parameters for the Ember route */ -const asyncRequestNodeList = (params, listURL, { queryParams }) => +const asyncRequestEntityQueryData = (params, listURL, { queryParamsKeys: queryParams }) => /** * Async thunk * @param {Function} dispatch * @return {Promise.<*>} */ async function(dispatch) { - const { entity, page, urn } = params; - const query = { page, urn }; + const { entity, page, urn, name } = params; + // Extract relevant query parameters into query object + const query = { page, urn, name }; dispatch(requestNodeList({ entity, listURL, query, queryParams })); + // For each entity fetch the list of nodes and the actual entities for the given query try { let nodesResult = {}, pagedEntities = {}; switch (entity) { case 'datasets': - [nodesResult, pagedEntities] = await [ + [nodesResult, pagedEntities] = await Promise.all([ dispatch(lazyRequestDatasetNodes({ listURL, query })), dispatch(lazyRequestUrnPagedDatasets({ query })) - ]; + ]); break; case 'metrics': + [nodesResult, pagedEntities] = await Promise.all([ + dispatch(lazyRequestMetricNodes({ listURL, query })), + dispatch(lazyRequestNamedPagedMetrics({ query })) + ]); + break; case 'flows': + [nodesResult, pagedEntities] = await Promise.all([ + dispatch(lazyRequestFlowsNodes({ listURL, query })), + dispatch(lazyRequestPagedUrnApplicationFlows({ query })) + ]); + break; default: return; } @@ -66,4 +80,4 @@ const asyncRequestNodeList = (params, listURL, { queryParams }) => } }; -export { ActionTypes, asyncRequestNodeList }; +export { ActionTypes, asyncRequestEntityQueryData }; diff --git a/wherehows-web/app/actions/entities/entities.js b/wherehows-web/app/actions/entities/entities.js index fb2d70a166..f7208a127a 100644 --- a/wherehows-web/app/actions/entities/entities.js +++ b/wherehows-web/app/actions/entities/entities.js @@ -40,10 +40,10 @@ const createAsyncThunk = ( * @return {Promise.<*>} */ ) => async (dispatch, getState) => { - const { status = 'error', data } = await asyncExecutor(getState); + const response = await asyncExecutor(getState); - if (status === 'ok') { - return dispatch(receiverActionCreator({ data })); + if (response.status === 'ok') { + return dispatch(receiverActionCreator(response)); } return dispatch(receiverActionCreator(new Error(`Request failed with status ${status}`))); diff --git a/wherehows-web/app/actions/entities/entity-request.js b/wherehows-web/app/actions/entities/entity-request.js index eb3266ebdc..2adb6c0f59 100644 --- a/wherehows-web/app/actions/entities/entity-request.js +++ b/wherehows-web/app/actions/entities/entity-request.js @@ -46,6 +46,79 @@ const fetchPagedUrnEntities = entity => getState => { }); }; +/** + * Takes a entity and returns a function to fetch entities by an entity url with a `name` segment + * @param {String} entity + */ +const fetchNamedPagedEntities = entity => getState => { + const { [entity]: { baseURL }, browseEntity: { [entity]: { query } } = {} } = getState(); + const queryCopy = Object.assign({}, query); + const name = queryCopy.name; + let baseNameUrl = baseURL; + + if (name) { + baseNameUrl = `${baseNameUrl}/name/${name}`; + delete queryCopy.name; + } + + const namedPageURL = Object.keys(queryCopy).reduce((url, queryKey) => { + let queryValue = queryCopy[queryKey]; + if (queryValue) { + return buildUrl(url, queryKey, queryValue); + } + + return url; + }, baseNameUrl); + + return fetch(namedPageURL).then(response => response.json()).then((payload = {}) => { + if (payload.status === 'ok') { + payload.data = Object.assign({}, payload.data, { + parentName: name || null + }); + } + + return payload; + }); +}; + +/** + * Takes a entity name and returns a function the fetches an entity by urn path segment + * @param {String} entity + */ +const fetchUrnPathEntities = entity => getState => { + const { [entity]: { baseURL }, browseEntity: { [entity]: { query } = {} } = {} } = getState(); + const queryCopy = Object.assign({}, query); + const urn = queryCopy.urn; + let baseUrnUrl = baseURL; + + // If the urn exists, append its value to the base url for the entity and remove the attribute from the local + // copy of the queried parameters + if (urn) { + baseUrnUrl = `${baseUrnUrl}/${urn}`; + delete queryCopy.urn; + } + + // Append the left over query params to the constructed url string + const urnPathUrl = Object.keys(query).reduce((url, queryKey) => { + let queryValue = query[queryKey]; + if (queryValue) { + return buildUrl(url, queryKey, queryValue); + } + + return url; + }, baseUrnUrl); + + return fetch(urnPathUrl).then(response => response.json()).then((payload = {}) => { + if (payload.status === 'ok') { + payload.data = Object.assign({}, payload.data, { + parentUrn: urn || null + }); + } + + return payload; + }); +}; + /** * Request urn child nodes/ datasets for the specified entity * @param entity @@ -75,7 +148,56 @@ const fetchNodes = entity => getState => { }, `${listURL}/${entity}`); // TODO: DSS-7019 remove any parsing from response objects. in createLazyRequest and update all call sites - return fetch(nodeURL).then(response => response.json()).then(({ status, nodes: data }) => ({ status, data })); + return fetch(nodeURL).then(response => response.json()).then((payload = {}) => { + return Object.assign({}, payload, { + parentUrn: query.urn || null + }); + }); }; -export { fetchPagedEntities, fetchPagedUrnEntities, fetchNodes }; +/** + * For a given entity name, fetches the nodes at the list url by appending the entity name and name path as segments + * of the request url + * @param {String} entity + */ +const fetchNamedEntityNodes = entity => getState => { + // TODO: DSS-7019 rename queryParams to queryList, don't over load the name `queryParams` + const { browseEntity: { [entity]: { listURL = '', query: { name } } = {} } = {} } = getState(); + const namePath = name ? `/${name}` : ''; + const nodeURL = `${listURL}/${entity}${namePath}`; + + return fetch(nodeURL).then(response => response.json()).then((payload = {}) => { + return Object.assign({}, payload, { + //TODO: Should this be namedEntityNodes vs urnPathEntityNodes + parentName: name || null + }); + }); +}; + +/** + * For a given entity, fetches the entities nodes at the list url by appending the entity name and the urn path + * as segments of the request url + * @param {String} entity + */ +const fetchUrnPathEntityNodes = entity => getState => { + const { browseEntity: { [entity]: { listURL = '', query: { urn } } = {} } = {} } = getState(); + const urnPath = urn ? `/${urn}` : ''; + const urnListUrl = `${listURL}/${entity}${urnPath}`; + + return fetch(urnListUrl).then(response => response.json()).then((payload = {}) => { + return Object.assign({}, payload, { + //TODO: Should this be namedEntityNodes vs urnPathEntityNodes + parentUrn: urn || null + }); + }); +}; + +export { + fetchPagedEntities, + fetchPagedUrnEntities, + fetchNodes, + fetchNamedPagedEntities, + fetchNamedEntityNodes, + fetchUrnPathEntities, + fetchUrnPathEntityNodes +}; diff --git a/wherehows-web/app/actions/entities/index.js b/wherehows-web/app/actions/entities/index.js index e66b8c571b..23acce739d 100644 --- a/wherehows-web/app/actions/entities/index.js +++ b/wherehows-web/app/actions/entities/index.js @@ -1,2 +1,2 @@ export * from 'wherehows-web/actions/entities/entities'; -export { fetchPagedEntities, fetchPagedUrnEntities, fetchNodes } from 'wherehows-web/actions/entities/entity-request'; +export * from 'wherehows-web/actions/entities/entity-request'; diff --git a/wherehows-web/app/actions/flows.js b/wherehows-web/app/actions/flows.js index 587c40b7c6..0647f0fcc5 100644 --- a/wherehows-web/app/actions/flows.js +++ b/wherehows-web/app/actions/flows.js @@ -1,5 +1,10 @@ import { createAction } from 'redux-actions'; -import { createLazyRequest, fetchPagedEntities } from 'wherehows-web/actions/entities'; +import { + createLazyRequest, + fetchPagedEntities, + fetchUrnPathEntityNodes, + fetchUrnPathEntities +} from 'wherehows-web/actions/entities'; import actionSet from 'wherehows-web/actions/action-set'; /** @@ -9,7 +14,13 @@ import actionSet from 'wherehows-web/actions/action-set'; const ActionTypes = { REQUEST_PAGED_FLOWS: actionSet('REQUEST_PAGED_FLOWS'), SELECT_PAGED_FLOWS: actionSet('SELECT_PAGED_FLOWS'), - RECEIVE_PAGED_FLOWS: actionSet('RECEIVE_PAGED_FLOWS') + RECEIVE_PAGED_FLOWS: actionSet('RECEIVE_PAGED_FLOWS'), + + REQUEST_PAGED_URN_FLOWS: actionSet('REQUEST_PAGED_URN_FLOWS'), + RECEIVE_PAGED_URN_FLOWS: actionSet('RECEIVE_PAGED_URN_FLOWS'), + + REQUEST_FLOWS_NODES: actionSet('REQUEST_FLOWS_NODES'), + RECEIVE_FLOWS_NODES: actionSet('RECEIVE_FLOWS_NODES') }; const requestPagedFlows = createAction(ActionTypes.REQUEST_PAGED_FLOWS); @@ -21,7 +32,28 @@ const receivePagedFlows = createAction( () => ({ receivedAt: Date.now() }) ); +const requestPagedUrnFlows = createAction(ActionTypes.REQUEST_PAGED_URN_FLOWS); + +const receivePagedUrnFlows = createAction( + ActionTypes.RECEIVE_PAGED_URN_FLOWS, + ({ data }) => data, + () => ({ receivedAt: Date.now() }) +); + +const requestFlowsNodes = createAction(ActionTypes.REQUEST_FLOWS_NODES); +const receiveFlowsNodes = createAction( + ActionTypes.RECEIVE_FLOWS_NODES, + response => response, + // meta data attached to the ActionTypes.RECEIVE_PAGED_FLOWS action + () => ({ receivedAt: Date.now() }) +); + // async action/thunk creator for ActionTypes.REQUEST_PAGED_FLOWS +// TODO: Is this redundant since we can use name without a name queryParam supplied const lazyRequestPagedFlows = createLazyRequest(requestPagedFlows, receivePagedFlows, fetchPagedEntities('flows')); -export { ActionTypes, lazyRequestPagedFlows }; +const lazyRequestFlowsNodes = createLazyRequest(requestFlowsNodes, receiveFlowsNodes, fetchUrnPathEntityNodes('flows')); + +const lazyRequestPagedUrnApplicationFlows = createLazyRequest(requestPagedUrnFlows, receivePagedUrnFlows, fetchUrnPathEntities('flows')); + +export { ActionTypes, lazyRequestPagedFlows, lazyRequestFlowsNodes, lazyRequestPagedUrnApplicationFlows }; diff --git a/wherehows-web/app/actions/metrics.js b/wherehows-web/app/actions/metrics.js index ac27de0e87..77d2a8f4b9 100644 --- a/wherehows-web/app/actions/metrics.js +++ b/wherehows-web/app/actions/metrics.js @@ -1,5 +1,10 @@ import { createAction } from 'redux-actions'; -import { createLazyRequest, fetchPagedEntities } from 'wherehows-web/actions/entities'; +import { + createLazyRequest, + fetchPagedEntities, + fetchNamedEntityNodes, + fetchNamedPagedEntities +} from 'wherehows-web/actions/entities'; import actionSet from 'wherehows-web/actions/action-set'; /** @@ -9,7 +14,13 @@ import actionSet from 'wherehows-web/actions/action-set'; const ActionTypes = { REQUEST_PAGED_METRICS: actionSet('REQUEST_PAGED_METRICS'), SELECT_PAGED_METRICS: actionSet('SELECT_PAGED_METRICS'), - RECEIVE_PAGED_METRICS: actionSet('RECEIVE_PAGED_METRICS') + RECEIVE_PAGED_METRICS: actionSet('RECEIVE_PAGED_METRICS'), + + REQUEST_PAGED_NAMED_METRICS: actionSet('REQUEST_PAGED_NAMED_METRICS'), + RECEIVE_PAGED_NAMED_METRICS: actionSet('RECEIVE_PAGED_NAMED_METRICS'), + + REQUEST_METRICS_NODES: actionSet('REQUEST_METRICS_NODES'), + RECEIVE_METRICS_NODES: actionSet('RECEIVE_METRICS_NODES') }; const requestPagedMetrics = createAction(ActionTypes.REQUEST_PAGED_METRICS); @@ -23,10 +34,47 @@ const receivePagedMetrics = createAction( () => ({ receivedAt: Date.now() }) ); +const requestPagedNamedMetrics = createAction(ActionTypes.REQUEST_PAGED_NAMED_METRICS); + +const receivePagedNamedMetrics = createAction( + ActionTypes.RECEIVE_PAGED_NAMED_METRICS, + ({ data }) => data, + () => ({ receivedAt: Date.now() }) +); + +const requestMetricNodes = createAction(ActionTypes.REQUEST_METRICS_NODES); +const receiveMetricNodes = createAction( + ActionTypes.RECEIVE_METRICS_NODES, + response => response, + // meta data attached to the ActionTypes.RECEIVE_PAGED_METRICS action + () => ({ receivedAt: Date.now() }) +); // async action/thunk creator for ActionTypes.REQUEST_PAGED_METRICS -const lazyRequestPagedMetrics = createLazyRequest(requestPagedMetrics, receivePagedMetrics, fetchPagedEntities('metrics')); +const lazyRequestPagedMetrics = createLazyRequest( + requestPagedMetrics, + receivePagedMetrics, + fetchPagedEntities('metrics') +); // async action/thunk creator for ActionTypes.SELECT_PAGED_METRICS -const lazySelectPagedMetrics = createLazyRequest(selectPagedMetrics, receivePagedMetrics, fetchPagedEntities('metrics')); +const lazySelectPagedMetrics = createLazyRequest( + selectPagedMetrics, + receivePagedMetrics, + fetchPagedEntities('metrics') +); -export { ActionTypes, lazyRequestPagedMetrics, lazySelectPagedMetrics }; +const lazyRequestMetricNodes = createLazyRequest(requestMetricNodes, receiveMetricNodes, fetchNamedEntityNodes('metrics')); + +const lazyRequestNamedPagedMetrics = createLazyRequest( + requestPagedNamedMetrics, + receivePagedNamedMetrics, + fetchNamedPagedEntities('metrics') +); + +export { + ActionTypes, + lazyRequestPagedMetrics, + lazySelectPagedMetrics, + lazyRequestMetricNodes, + lazyRequestNamedPagedMetrics +}; diff --git a/wherehows-web/app/components/ace-editor.js b/wherehows-web/app/components/ace-editor.js index 76ddc169f5..729763f708 100644 --- a/wherehows-web/app/components/ace-editor.js +++ b/wherehows-web/app/components/ace-editor.js @@ -44,15 +44,11 @@ export default Ember.Component.extend({ var url = this.get('savePath'); url = url.replace(/\{.\w+\}/, this.get('itemId')) var method = 'POST'; - var token = $("#csrfToken").val().replace('/', ''); - var data = {"csrfToken": token}; + var data = {}; data[this.get('saveParam')] = this.editor.getSession().getValue() $.ajax({ url: url, method: method, - headers: { - 'Csrf-Token': token - }, dataType: 'json', data: data }).done(function (data, txt, xhr) { diff --git a/wherehows-web/app/components/browser/containers/browser-rail.js b/wherehows-web/app/components/browser/containers/browser-rail.js index 81f749465e..c96e2c9cd4 100644 --- a/wherehows-web/app/components/browser/containers/browser-rail.js +++ b/wherehows-web/app/components/browser/containers/browser-rail.js @@ -1,6 +1,6 @@ import Ember from 'ember'; import connect from 'ember-redux/components/connect'; -import { urnRegex } from 'wherehows-web/utils/validators/urn'; +import { urnRegex, specialFlowUrnRegex } from 'wherehows-web/utils/validators/urn'; const { Component } = Ember; @@ -10,13 +10,7 @@ const { Component } = Ember; * @type {RegExp} */ const pageRegex = /\/page\/([0-9]+)/i; -/** - * Matches a url string path segment that optionally starts with a hash followed by forward slash, - * either datasets or flows or metrics, forward slash, number of varying length and optional trailing slash - * The number is retained - * @type {RegExp} - */ -const entityRegex = /^#?\/(?:datasets|metrics|flows)\/([0-9]+)\/?/; +const nameRegex = /\/name\/([0-9a-z()_{}\[\]\/\s]+)/i; /** * Takes a node url and parses out the query params and path spec to be included in the link component @@ -26,6 +20,8 @@ const entityRegex = /^#?\/(?:datasets|metrics|flows)\/([0-9]+)\/?/; const nodeUrlToQueryParams = nodeUrl => { const pageMatch = nodeUrl.match(pageRegex); const urnMatch = nodeUrl.match(urnRegex); + const flowUrnMatch = nodeUrl.match(specialFlowUrnRegex); + const nameMatch = nodeUrl.match(nameRegex); let queryParams = null; // If we have a page match, append the page number to eventual urn object @@ -37,17 +33,41 @@ const nodeUrlToQueryParams = nodeUrl => { }); } + if (Array.isArray(nameMatch)) { + let match = nameMatch[1]; + match = match.split('/page')[0]; + + queryParams = Object.assign({}, queryParams, { + name: match + }); + } + // If we have a urn match, append the urn to eventual query params object - if (Array.isArray(urnMatch)) { + if (Array.isArray(urnMatch) || Array.isArray(flowUrnMatch)) { + const urn = urnMatch || [flowUrnMatch[1]]; + queryParams = Object.assign({}, queryParams, { // Extract the entire match as urn value - urn: urnMatch[0] + urn: urn[0] }); } return queryParams; }; +const getNodes = (entity, state = {}) => query => + ({ + get datasets() { + return state[entity].nodesByUrn[query]; + }, + get metrics() { + return state[entity].nodesByName[query]; + }, + get flows() { + return this.datasets; + } + }[entity]); + /** * Selector function that takes a Redux Store to extract * state props for the browser-rail @@ -58,36 +78,52 @@ const stateToComputed = state => { // Extracts the current entity active in the browse view const { browseEntity: { currentEntity = '' } = {} } = state; // Retrieves properties for the current entity from the state tree - const { [currentEntity]: { nodesByUrn = {}, query: { urn } = {} } } = state; + const { browseEntity: { [currentEntity]: { query: { urn, name } } } } = state; // Removes `s` from the end of each entity name. Ember routes for individual entities are singular, and also // returned entities contain id prop that is the singular type name, suffixed with Id, e.g. metricId // datasets -> dataset, metrics -> metric, flows -> flow const singularName = currentEntity.slice(0, -1); - let nodes = nodesByUrn[urn] || []; + const query = + { + datasets: urn, + metrics: name, + flows: urn + }[currentEntity] || null; + let nodes = getNodes(currentEntity, state)(query) || []; /** * Creates dynamic query link params for each node * @type {Array} list of child nodes or datasets to render */ - nodes = nodes.map(({ nodeName, nodeUrl, [`${singularName}Id`]: id }) => { + nodes = nodes.map(({ nodeName, nodeUrl, [`${singularName}Id`]: id, application = '' }) => { nodeUrl = String(nodeUrl); + const node = { + title: nodeName, + text: nodeName + }; // If the id prop (datasetId|metricId|flowId) is truthy, then it is a standalone entity if (id) { - return { - title: nodeName, - text: nodeName, + let idNode = Object.assign({}, node); + + if (singularName === 'flow' && application) { + idNode = Object.assign({}, idNode, { + queryParams: { + name: application + } + }); + } + + return Object.assign({}, idNode, { route: `${currentEntity}.${singularName}`, - model: nodeUrl.match(entityRegex)[1] - }; + model: id + }); } - return { - title: nodeName, - text: nodeName, + return Object.assign({}, node, { route: `browse.entity`, model: currentEntity, queryParams: nodeUrlToQueryParams(nodeUrl) - }; + }); }); return { nodes }; diff --git a/wherehows-web/app/components/browser/containers/browser-viewport.js b/wherehows-web/app/components/browser/containers/browser-viewport.js index 0615947bce..0d7b1b67c1 100644 --- a/wherehows-web/app/components/browser/containers/browser-viewport.js +++ b/wherehows-web/app/components/browser/containers/browser-viewport.js @@ -3,6 +3,25 @@ import connect from 'ember-redux/components/connect'; const { Component } = Ember; +/** + * Extract the childIds for an entity under a specific category + * @param entity + * @param state + */ +const getChildIds = (entity, state = {}) => query => + ({ + get datasets() { + return state[entity].byUrn[query]; + }, + get metrics() { + return state[entity].byName[query]; + }, + get flows() { + // Flows are retrieved by name as well + return this.datasets; + } + }[entity]); + /** * Selector function that takes a Redux Store to extract * state props for the browser-rail @@ -12,12 +31,17 @@ const stateToComputed = state => { // Extracts the current entity active in the browse view const { browseEntity: { currentEntity = '' } = {} } = state; // Retrieves properties for the current entity from the state tree - let { browseEntity: { [currentEntity]: { query: { urn } } } } = state; + const { browseEntity: { [currentEntity]: { query: { urn, name } } } } = state; // Default urn to null, which represents the top-level parent - urn = urn || null; + const query = + { + datasets: urn, + metrics: name, + flows: urn + }[currentEntity] || null; // Read the list of ids child entity ids associated with the urn - const { [currentEntity]: { byUrn: { [urn]: childIds = [] } } } = state; + const childIds = getChildIds(currentEntity, state)(query) || []; // Read the list of entities, stored in the byId property const { [currentEntity]: { byId: entities } } = state; /** diff --git a/wherehows-web/app/components/dataset-confidential.js b/wherehows-web/app/components/dataset-confidential.js index 112ffb2491..721a90e1a3 100644 --- a/wherehows-web/app/components/dataset-confidential.js +++ b/wherehows-web/app/components/dataset-confidential.js @@ -41,13 +41,13 @@ const datasetClassifiersKeys = Object.keys(datasetClassifiers); // TODO: DSS-6671 Extract to constants module const successUpdating = 'Your changes have been successfully saved!'; const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.'; -const missingTypes = 'Looks like some fields are marked as `Confidential` or ' + - '`Highly Confidential` but do not have a specified `Field Format`?'; +const missingTypes = + 'Looks like some fields are marked as `Confidential` or `Highly Confidential` but do not have a specified `Field Format`?'; const hiddenTrackingFieldsMsg = htmlSafe( '

Hey! Just a heads up that some fields in this dataset have been hidden from the table(s) below. ' + - 'These are tracking fields for which we\'ve been able to predetermine the compliance classification.

' + - '

For example: header.memberId, requestHeader. ' + - 'Hopefully, this saves you some scrolling!

' + "These are tracking fields for which we've been able to predetermine the compliance classification.

" + + '

For example: header.memberId, requestHeader. ' + + 'Hopefully, this saves you some scrolling!

' ); /** @@ -55,8 +55,7 @@ const hiddenTrackingFieldsMsg = htmlSafe( * for now, so no need to make into a helper * @param {String} string */ -const formatAsCapitalizedStringWithSpaces = string => - string.replace(/[A-Z]/g, match => ` ${match}`).capitalize(); +const formatAsCapitalizedStringWithSpaces = string => string.replace(/[A-Z]/g, match => ` ${match}`).capitalize(); export default Component.extend({ sortColumnWithName: 'identifierField', @@ -73,10 +72,9 @@ export default Component.extend({ // Map logicalTypes to options better consumed by drop down logicalTypes: ['', ...logicalTypes.sort()].map(value => { - const label = value ? - value.replace(/_/g, ' ') - .replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize()) : - 'Not Specified'; + const label = value + ? value.replace(/_/g, ' ').replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize()) + : 'Not Specified'; return { value, @@ -122,25 +120,26 @@ export default Component.extend({ */ fieldNameToClass: computed( `${sourceClassificationKey}.{confidential,limitedDistribution,highlyConfidential}.[]`, - function () { + function() { const sourceClasses = getWithDefault(this, sourceClassificationKey, []); // Creates a lookup table of fieldNames to classification // Also, the expectation is that the association from fieldName -> classification // is one-to-one hence no check to ensure a fieldName gets clobbered // in the lookup assignment - return Object.keys(sourceClasses) - .reduce((lookup, classificationKey) => - // For the provided classificationKey, iterate over it's fieldNames, - // and assign the classificationKey to the fieldName in the table - (sourceClasses[classificationKey] || []).reduce((lookup, field) => { - const { identifierField } = field; - // cKey -> 1...fieldNameList => fieldName -> cKey - lookup[identifierField] = classificationKey; - return lookup; - }, lookup), - {} - ); - }), + return Object.keys(sourceClasses).reduce( + (lookup, classificationKey) => + // For the provided classificationKey, iterate over it's fieldNames, + // and assign the classificationKey to the fieldName in the table + (sourceClasses[classificationKey] || []).reduce((lookup, field) => { + const { identifierField } = field; + // cKey -> 1...fieldNameList => fieldName -> cKey + lookup[identifierField] = classificationKey; + return lookup; + }, lookup), + {} + ); + } + ), /** * Lists all the dataset fields found in the `columns` api, and intersects @@ -166,10 +165,9 @@ export default Component.extend({ // assign to field, otherwise null // Rather than assigning the default classification here, nulling gives the benefit of allowing // subsequent consumer know that this field did not have a previous classification - const field = classification ? - get(this, `${sourceClassificationKey}.${classification}`) - .findBy('identifierField', identifierField) : - null; + const field = classification + ? get(this, `${sourceClassificationKey}.${classification}`).findBy('identifierField', identifierField) + : null; // Extract the logicalType from the field const logicalType = isPresent(field) ? field.logicalType : null; @@ -190,21 +188,22 @@ export default Component.extend({ * tracking header. * Used to indicate to viewer that these fields are hidden. */ - containsHiddenTrackingFields: computed( - 'classificationDataFieldsSansHiddenTracking.length', - function () { - // If their is a diff in complianceDataFields and complianceDataFieldsSansHiddenTracking, - // then we have hidden tracking fields - return get(this, 'classificationDataFieldsSansHiddenTracking.length') !== get(this, 'classificationDataFields.length'); - }), + containsHiddenTrackingFields: computed('classificationDataFieldsSansHiddenTracking.length', function() { + // If their is a diff in complianceDataFields and complianceDataFieldsSansHiddenTracking, + // then we have hidden tracking fields + return ( + get(this, 'classificationDataFieldsSansHiddenTracking.length') !== get(this, 'classificationDataFields.length') + ); + }), /** * @type {Array.} Filters the mapped confidential data fields without `kafka type` * tracking headers */ - classificationDataFieldsSansHiddenTracking: computed('classificationDataFields.[]', function () { - return get(this, 'classificationDataFields') - .filter(({ identifierField }) => !isTrackingHeaderField(identifierField)); + classificationDataFieldsSansHiddenTracking: computed('classificationDataFields.[]', function() { + return get(this, 'classificationDataFields').filter( + ({ identifierField }) => !isTrackingHeaderField(identifierField) + ); }), /** @@ -219,20 +218,19 @@ export default Component.extend({ .then(({ status = 'error' }) => { // The server api currently responds with an object containing // a status when complete - return status === 'ok' ? - setProperties(this, { - _message: successMessage || successUpdating, - _alertType: 'success' - }) : - Promise.reject(new Error(`Reason code for this is ${status}`)); + return status === 'ok' + ? setProperties(this, { + _message: successMessage || successUpdating, + _alertType: 'success' + }) + : Promise.reject(new Error(`Reason code for this is ${status}`)); }) .catch(err => { let _message = `${failedUpdating} \n ${err}`; let _alertType = 'danger'; if (get(this, 'isNewSecuritySpecification')) { - _message = 'This dataset does not have any ' + - 'previously saved fields with a Security Classification.'; + _message = 'This dataset does not have any previously saved fields with a Security Classification.'; _alertType = 'info'; } @@ -264,7 +262,11 @@ export default Component.extend({ const nextProps = { identifierField, logicalType }; // The current classification name for the candidate identifier const currentClassLookup = get(this, 'fieldNameToClass'); - const defaultClassification = getWithDefault(this, `${sourceClassificationKey}.${defaultFieldDataTypeClassification[logicalType]}`, []); + const defaultClassification = getWithDefault( + this, + `${sourceClassificationKey}.${defaultFieldDataTypeClassification[logicalType]}`, + [] + ); let currentClassificationName = currentClassLookup[identifierField]; /** @@ -278,11 +280,11 @@ export default Component.extend({ const currentClassification = get(this, `${sourceClassificationKey}.${currentClassificationName}`); if (!Array.isArray(currentClassification)) { - throw new Error(` - You have specified a classification object that is not a list ${currentClassification}. + throw new Error( + `You have specified a classification object that is not a list ${currentClassification}. Ensure that the classification for this identifierField (${identifierField}) is - set before attempting to change the logicalType. - `); + set before attempting to change the logicalType.` + ); } const field = currentClassification.findBy('identifierField', identifierField); @@ -292,8 +294,7 @@ export default Component.extend({ if (isPresent(field)) { // Remove identifierField from list currentClassification.setObjects( - currentClassification.filter( - ({ identifierField: fieldName }) => fieldName !== identifierField) + currentClassification.filter(({ identifierField: fieldName }) => fieldName !== identifierField) ); } @@ -343,24 +344,17 @@ export default Component.extend({ // in any other classification lists by checking that the lookup is void if (!isBlank(currentClass)) { // Get the current classification list - const currentClassification = get( - this, - `${sourceClassificationKey}.${currentClass}` - ); + const currentClassification = get(this, `${sourceClassificationKey}.${currentClass}`); // Remove identifierField from list currentClassification.setObjects( - currentClassification.filter( - ({ identifierField: fieldName }) => fieldName !== identifierField) + currentClassification.filter(({ identifierField: fieldName }) => fieldName !== identifierField) ); } if (classKey) { // Get the candidate list - let classification = get( - this, - `${sourceClassificationKey}.${classKey}` - ); + let classification = get(this, `${sourceClassificationKey}.${classKey}`); // In the case that the list is not pre-populated, // the value will be the default null, array ops won't work here // ...so make array @@ -400,9 +394,8 @@ export default Component.extend({ * @type {Boolean} */ const classedFieldsHaveLogicalType = classifiers.every(classifier => - this.ensureFieldsContainLogicalType( - getWithDefault(this, `${sourceClassificationKey}.${classifier}`, []) - )); + this.ensureFieldsContainLogicalType(getWithDefault(this, `${sourceClassificationKey}.${classifier}`, [])) + ); if (classedFieldsHaveLogicalType) { this.whenRequestCompletes(get(this, 'onSave')()); diff --git a/wherehows-web/app/components/metric-detail.js b/wherehows-web/app/components/metric-detail.js index 926b613009..07ece330a0 100644 --- a/wherehows-web/app/components/metric-detail.js +++ b/wherehows-web/app/components/metric-detail.js @@ -1,4 +1,22 @@ import Ember from 'ember'; -export default Ember.Component.extend({ +const { Component, get } = Ember; + +export default Component.extend({ + didInsertElement() { + this._super(...arguments); + const metric = get(this, 'model'); + + if (metric) { + self.initializeXEditable( + metric.id, + metric.description, + metric.dashboardName, + metric.sourceType, + metric.grain, + metric.displayFactor, + metric.displayFactorSym + ); + } + } }); diff --git a/wherehows-web/app/components/metric-watch.js b/wherehows-web/app/components/metric-watch.js index 1e27ad5dfe..11e9d3f2b8 100644 --- a/wherehows-web/app/components/metric-watch.js +++ b/wherehows-web/app/components/metric-watch.js @@ -23,7 +23,7 @@ export default Ember.Component.extend({ } }).done(function (data, txt, xhr) { _this.set('metric.watchId', data.watchId) - _this.sendAction('getMetrics') + // _this.sendAction('getMetrics') }).fail(function (xhr, txt, err) { console.log('Error: Could not watch metric.') }) diff --git a/wherehows-web/app/controllers/browse/entity.js b/wherehows-web/app/controllers/browse/entity.js index ef26978d68..afd6c98136 100644 --- a/wherehows-web/app/controllers/browse/entity.js +++ b/wherehows-web/app/controllers/browse/entity.js @@ -5,7 +5,9 @@ const { Controller } = Ember; * Handles query params for browse.entity route */ export default Controller.extend({ - queryParams: ['page', 'urn'], + queryParams: ['page', 'urn', 'size', 'name'], page: 1, - urn: '' + urn: '', + name: '', + size: 10 }); diff --git a/wherehows-web/app/controllers/flows/flow.js b/wherehows-web/app/controllers/flows/flow.js new file mode 100644 index 0000000000..86552655e1 --- /dev/null +++ b/wherehows-web/app/controllers/flows/flow.js @@ -0,0 +1,6 @@ +import Ember from 'ember'; +const { Controller } = Ember; + +export default Controller.extend({ + queryParams: ['name'] +}); diff --git a/wherehows-web/app/reducers/datasets.js b/wherehows-web/app/reducers/datasets.js index bf6d3dadea..55f839de78 100644 --- a/wherehows-web/app/reducers/datasets.js +++ b/wherehows-web/app/reducers/datasets.js @@ -1,12 +1,20 @@ import { initializeState, - receiveNodes, + urnsToNodeUrn, receiveEntities, createUrnMapping, createPageMapping } from 'wherehows-web/reducers/entities'; import { ActionTypes } from 'wherehows-web/actions/datasets'; +/** + * Sets the default initial state for metrics slice. Appends a byName property to the shared representation. + * @type {Object} + */ +const initialState = Object.assign({}, initializeState(), { + nodesByUrn: [] +}); + /** * datasets root reducer * Takes the `datasets` slice of the state tree and performs the specified reductions for each action @@ -15,14 +23,14 @@ import { ActionTypes } from 'wherehows-web/actions/datasets'; * @prop {String} action.type actionType * @return {Object} */ -export default (state = initializeState(), action = {}) => { +export default (state = initialState, action = {}) => { switch (action.type) { // Action indicating a request for datasets by page case ActionTypes.SELECT_PAGED_DATASETS: case ActionTypes.REQUEST_PAGED_DATASETS: return Object.assign({}, state, { query: Object.assign({}, state.query, { - page: action.payload.page + page: action.payload.query.page }), baseURL: action.payload.baseURL || state.baseURL, isFetching: true @@ -51,7 +59,7 @@ export default (state = initializeState(), action = {}) => { case ActionTypes.RECEIVE_DATASET_NODES: // Action indicating a receipt of list nodes / datasets for dataset urn return Object.assign({}, state, { isFetching: false, - nodesByUrn: receiveNodes(state, action.payload) + nodesByUrn: urnsToNodeUrn(state, action.payload) }); default: diff --git a/wherehows-web/app/reducers/entities/entities.js b/wherehows-web/app/reducers/entities/entities.js index d58c0d04d3..45b02018e7 100644 --- a/wherehows-web/app/reducers/entities/entities.js +++ b/wherehows-web/app/reducers/entities/entities.js @@ -52,6 +52,15 @@ const appendUrnIdMap = ( [parentUrn]: union(urnEntities[parentUrn], entities.mapBy('id')) }); +/** + * Appends a list of child entities ids for a given name. name is null for top level entities + * @param {String} entityName + */ +const appendNamedIdMap = entityName => (nameEntities, { parentName = null, [entityName]: entities = [] }) => + Object.assign({}, nameEntities, { + [parentName]: union(nameEntities[parentName], entities.mapBy('id')) + }); + /** * Returns a curried function that receives entityName to lookup on the props object * @param {String} entityName @@ -79,12 +88,27 @@ const entitiesToPage = ( * Maps a urn to a list of child nodes from the list api * @param {Object} state * @param {Array} nodes - * @return {Object} + * @param {String} parentUrn + * @return {*} */ -const urnsToNodeUrn = (state, { data: nodes = [] }) => { - const { query: { urn }, nodesByUrn } = state; +const urnsToNodeUrn = (state, { nodes = [], parentUrn = null } = {}) => { + const { nodesByUrn } = state; return Object.assign({}, nodesByUrn, { - [urn]: union(nodes) + [parentUrn]: union(nodes) + }); +}; + +/** + * Maps a name to a list of child nodes from the list api + * @param {Object} state + * @param {Array} nodes + * @param {String} parentName + * @return {*} + */ +const namesToNodeName = (state, { nodes = [], parentName = null } = {}) => { + const { nodesByName } = state; + return Object.assign({}, nodesByName, { + [parentName]: union(nodes) }); }; @@ -112,20 +136,27 @@ const createUrnMapping = entityName => (state, payload = {}) => { }; /** - * + * Curries an entityName into a function to append named entity entities + * @param {String} entityName + */ +const createNameMapping = entityName => (state, payload = {}) => { + return appendNamedIdMap(entityName)(state, payload); +}; + +/** + * Appends a list of entities to a page * @param {String} entityName */ const createPageMapping = entityName => (state, payload = {}) => { return entitiesToPage(entityName)(state, payload); }; -/** - * Takes the response from the list api request and invokes a function to - * map a urn to child urns or nodes - * @param {Object} state - * @param {Object} payload the response from the list endpoint/api - * @return {Object} - */ -const receiveNodes = (state, payload = {}) => urnsToNodeUrn(state, payload); - -export { receiveNodes, initializeState, receiveEntities, createUrnMapping, createPageMapping }; +export { + initializeState, + namesToNodeName, + urnsToNodeUrn, + receiveEntities, + createUrnMapping, + createPageMapping, + createNameMapping +}; diff --git a/wherehows-web/app/reducers/flows.js b/wherehows-web/app/reducers/flows.js index f27c64e3a7..7ff768f2da 100644 --- a/wherehows-web/app/reducers/flows.js +++ b/wherehows-web/app/reducers/flows.js @@ -1,6 +1,19 @@ -import { initializeState, createUrnMapping, receiveEntities, createPageMapping } from 'wherehows-web/reducers/entities'; +import { + initializeState, + createUrnMapping, + receiveEntities, + createPageMapping, + urnsToNodeUrn +} from 'wherehows-web/reducers/entities'; import { ActionTypes } from 'wherehows-web/actions/flows'; +/** + * Sets the default initial state for flows slice. Appends a byName property to the shared representation. + * @type {Object} + */ +const initialState = Object.assign({}, initializeState(), { + nodesByUrn: [] +}); /** * Takes the `flows` slice of the state tree and performs the specified reductions for each action * @param {Object} state = initialState the slice of the state object representing flows @@ -8,23 +21,41 @@ import { ActionTypes } from 'wherehows-web/actions/flows'; * @prop {String} action.type actionType * @return {Object} */ -export default (state = initializeState(), action = {}) => { +export default (state = initialState, action = {}) => { switch (action.type) { case ActionTypes.RECEIVE_PAGED_FLOWS: + case ActionTypes.RECEIVE_PAGED_URN_FLOWS: return Object.assign({}, state, { isFetching: false, - byUrn: createUrnMapping('flows')(state.byUrn, action.payload), byId: receiveEntities('flows')(state.byId, action.payload), - byPage: createPageMapping('flows')(state.byPage, action.payload) + byPage: createPageMapping('flows')(state.byPage, action.payload), + byUrn: createUrnMapping('flows')(state.byUrn, action.payload) + }); + + case ActionTypes.REQUEST_FLOWS_NODES: + return Object.assign({}, state, { + query: Object.assign({}, state.query, action.payload.query), + isFetching: true + }); + + case ActionTypes.RECEIVE_FLOWS_NODES: + return Object.assign({}, state, { + isFetching: false, + nodesByUrn: urnsToNodeUrn(state, action.payload) + }); + + case ActionTypes.REQUEST_PAGED_URN_FLOWS: + return Object.assign({}, state, { + query: Object.assign({}, state.query, action.payload.query) }); case ActionTypes.SELECT_PAGED_FLOWS: case ActionTypes.REQUEST_PAGED_FLOWS: return Object.assign({}, state, { query: Object.assign({}, state.query, { - page: action.payload.page + page: action.payload.query.page }), - baseURL: action.payload.baseURL, + baseURL: action.payload.baseURL || state.baseURL, isFetching: true }); diff --git a/wherehows-web/app/reducers/metrics.js b/wherehows-web/app/reducers/metrics.js index cd8ec79c43..a27a3df365 100644 --- a/wherehows-web/app/reducers/metrics.js +++ b/wherehows-web/app/reducers/metrics.js @@ -1,13 +1,19 @@ -import { initializeState, createUrnMapping, receiveEntities, createPageMapping } from 'wherehows-web/reducers/entities'; +import { + initializeState, + receiveEntities, + namesToNodeName, + createPageMapping, + createNameMapping +} from 'wherehows-web/reducers/entities'; import { ActionTypes } from 'wherehows-web/actions/metrics'; - /** * Sets the default initial state for metrics slice. Appends a byName property to the shared representation. * @type {Object} */ const initialState = Object.assign({}, initializeState(), { - byName: {} + byName: {}, + nodesByName: [] }); /** @@ -23,19 +29,36 @@ export default (state = initialState, action = {}) => { case ActionTypes.SELECT_PAGED_METRICS: case ActionTypes.REQUEST_PAGED_METRICS: return Object.assign({}, state, { - query: Object.assign({}, state.query, { - page: action.payload.page - }), + query: Object.assign({}, state.query, action.payload.query), baseURL: action.payload.baseURL || state.baseURL, isFetching: true }); + // Action indicating a receipt of metrics by page + case ActionTypes.REQUEST_PAGED_NAMED_METRICS: + return Object.assign({}, state, { + query: Object.assign({}, state.query, action.payload.query) + }); + case ActionTypes.RECEIVE_PAGED_METRICS: + case ActionTypes.RECEIVE_PAGED_NAMED_METRICS: return Object.assign({}, state, { isFetching: false, - byUrn: createUrnMapping('metrics')(state.byUrn, action.payload), byId: receiveEntities('metrics')(state.byId, action.payload), - byPage: createPageMapping('metrics')(state.byPage, action.payload) + byPage: createPageMapping('metrics')(state.byPage, action.payload), + byName: createNameMapping('metrics')(state.byName, action.payload) + }); + + case ActionTypes.REQUEST_METRICS_NODES: + return Object.assign({}, state, { + query: Object.assign({}, state.query, action.payload.query), + isFetching: true + }); + + case ActionTypes.RECEIVE_METRICS_NODES: // Action indicating a receipt of list nodes / datasets for dataset urn + return Object.assign({}, state, { + isFetching: false, + nodesByName: namesToNodeName(state, action.payload) }); default: diff --git a/wherehows-web/app/routes/browse.js b/wherehows-web/app/routes/browse.js index aca2ad407b..5eead504ec 100644 --- a/wherehows-web/app/routes/browse.js +++ b/wherehows-web/app/routes/browse.js @@ -8,9 +8,9 @@ const { Route } = Ember; // TODO: Route should transition to browse/entity, pay attention to the fact that // this route initializes store with entity metrics on entry const entityUrls = { - datasets: '/api/v1/datasets?size=10', - metrics: '/api/v1/metrics?size=10', - flows: '/api/v1/flows?size=10' + datasets: '/api/v1/datasets', + metrics: '/api/v1/metrics', + flows: '/api/v1/flows' }; export default route({ diff --git a/wherehows-web/app/routes/browse/entity.js b/wherehows-web/app/routes/browse/entity.js index fa94d2e08e..5142a34d0c 100644 --- a/wherehows-web/app/routes/browse/entity.js +++ b/wherehows-web/app/routes/browse/entity.js @@ -1,15 +1,21 @@ import Ember from 'ember'; import route from 'ember-redux/route'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; -import { asyncRequestNodeList } from 'wherehows-web/actions/browse/entity'; +import { asyncRequestEntityQueryData } from 'wherehows-web/actions/browse/entity'; const { Route } = Ember; +// TODO: DSS-6581 Create URL retrieval module const listUrl = '/api/v1/list'; -const queryParams = ['page', 'urn']; +const queryParamsKeys = ['page', 'urn', 'name']; +/** + * Creates a route handler for browse.entity route + * entity can be any (datasets|metrics|flows) + * @type {Ember.Route} + */ const BrowseEntityRoute = Route.extend(AuthenticatedRouteMixin, { - queryParams: queryParams.reduce( + queryParams: queryParamsKeys.reduce( (queryParams, param) => Object.assign({}, queryParams, { [param]: { refreshModel: true } @@ -18,6 +24,15 @@ const BrowseEntityRoute = Route.extend(AuthenticatedRouteMixin, { ) }); +/** + * Ember Redux decorator wraps incoming route object and injects the redux store.dispatch method as the + * first argument + */ export default route({ - model: (dispatch, params) => dispatch(asyncRequestNodeList(params, listUrl, { queryParams })) + /** + * + * @param dispatch + * @param params + */ + model: (dispatch, params) => dispatch(asyncRequestEntityQueryData(params, listUrl, { queryParamsKeys })) })(BrowseEntityRoute); diff --git a/wherehows-web/app/routes/flows/flow.js b/wherehows-web/app/routes/flows/flow.js index 26d9f3124e..e605f86173 100644 --- a/wherehows-web/app/routes/flows/flow.js +++ b/wherehows-web/app/routes/flows/flow.js @@ -1,4 +1,44 @@ import Ember from 'ember'; +import fetch from 'ember-network/fetch'; -export default Ember.Route.extend({ +const { Route, setProperties } = Ember; + +const flowUrlRoot = 'api/v1/flow'; + +/** + * Takes an object representing a flow and generates a list of breadcrumb items for navigating this hierarchy + * @param {Object} props properties for the current flow + * @return {[*,*,*]} + */ +const makeFlowsBreadcrumbs = (props = {}) => { + const { name: application, id, flow } = props; + return [ + { crumb: 'Flows', name: '', urn: '' }, + { crumb: application, name: application, urn: '' }, + { crumb: flow, flow_id: id, urn: application } + ]; +}; + +export default Route.extend({ + setupController: function(controller, model = {}) { + setProperties(controller, { + model, + breadcrumbs: makeFlowsBreadcrumbs(model.data) + }); + + }, + + model: async (params = {}) => { + const { flow_id, name } = params; + const flowsUrl = `${flowUrlRoot}/${name}/${flow_id}`; + let response = await fetch(flowsUrl).then(response => response.json()); + + if (response && response.status === 'ok') { + response.data = Object.assign({}, response.data, { + name + }); + } + + return response; + } }); diff --git a/wherehows-web/app/routes/metrics/metric.js b/wherehows-web/app/routes/metrics/metric.js index 7ff37e689a..bba040a1c5 100644 --- a/wherehows-web/app/routes/metrics/metric.js +++ b/wherehows-web/app/routes/metrics/metric.js @@ -1,111 +1,48 @@ import Ember from 'ember'; +import fetch from 'ember-network/fetch'; -export default Ember.Route.extend({ - setupController: function (controller, model) { - const metricsController = this.controllerFor('metrics'); +const { Route, setProperties } = Ember; - if (metricsController) { - metricsController.set('detailview', true); - } - currentTab = 'Metrics'; - updateActiveTab(); - var name; - var id = 0; - if (model && model.id) { - var url = 'api/v1/metrics/' + model.id; - id = model.id; - if (model.category) { - name = '{' + model.category + '} ' + model.name; - } - else { - name = model.name; - } - var breadcrumbs; - $.get(url, function (data) { - if (data && data.status == "ok") { - controller.set("model", data.metric); - var dashboard = data.metric.dashboardName; - if (!dashboard) { - dashboard = '(Other)'; - } - var group = data.metric.group; - if (!group) { - group = '(Other)'; - } - breadcrumbs = [{"title": "METRICS_ROOT", "urn": "page/1"}, - {"title": dashboard, "urn": "name/" + dashboard + "/page/1"}, - {"title": group, "urn": "name/" + dashboard + "/" + group + "/page/1"}, - {"title": data.metric.name, "urn": model.id}]; - controller.set('breadcrumbs', breadcrumbs); - setTimeout(initializeXEditable(id, - data.metric.description, - data.metric.dashboardName, - data.metric.sourceType, - data.metric.grain, - data.metric.displayFactor, - data.metric.displayFactorSym), 500); - } - }); - } - else if (model && model.metric) { - id = model.metric.id; - var dashboard = model.metric.dashboardName; - if (!dashboard) { - dashboard = '(Other)'; - } - var group = model.metric.group; - if (!group) { - group = '(Other)'; - } - breadcrumbs = [{"title": "METRICS_ROOT", "urn": "page/1"}, - {"title": name, "urn": "name/" + dashboard + "/page/1"}, - {"title": group, "urn": "name/" + dashboard + "/" + group + "/page/1"}, - {"title": model.metric.name, "urn": model.id}]; - controller.set('breadcrumbs', breadcrumbs); - if (model.metric.category) { - name = '{' + model.metric.category + '} ' + model.metric.name; - } - else { - name = model.metric.name; - } - setTimeout(initializeXEditable(id, - model.metric.description, - model.metric.dashboardName, - model.metric.sourceType, - model.metric.grain, - model.metric.displayFactor, - model.metric.displayFactorSym), 500); - } +const metricsUrlRoot = '/api/v1/metrics'; +/** + * Takes an object representing a metric and generates a list of breadcrumb items for each level in the + * hierarchy + * @param {Object} metric properties for the current metric + * @return {[*,*,*,*]} + */ +const makeMetricsBreadcrumbs = (metric = {}) => { + let { id, dashboardName, group, category, name } = metric; + dashboardName || (dashboardName = '(Other)'); + group || (group = '(Other)'); + name = category ? `{${category}} ${name}` : name; - var listUrl = 'api/v1/list/metric/' + id; - $.get(listUrl, function (data) { - if (data && data.status == "ok") { - // renderMetricListView(data.nodes, id); - } + return [ + { crumb: 'Metrics', name: '' }, + { crumb: dashboardName, name: dashboardName }, + { crumb: group, name: `${dashboardName}/${group}` }, + { crumb: name, name: id } + ]; +}; + +export default Route.extend({ + setupController(controller, model) { + const { metric } = model; + + // Set the metric as the model and create breadcrumbs + setProperties(controller, { + model: metric, + breadcrumbs: makeMetricsBreadcrumbs(metric) }); }, - actions: { - getMetrics: function () { - var id = this.get('controller.model.id'); - var listUrl = 'api/v1/list/metrics/' + id; - $.get(listUrl, function (data) { - if (data && data.status == "ok") { - // renderMetricListView(data.nodes, id); - } - }); - - var url = 'api/v1/metrics/' + this.get('controller.model.id') - var _this = this - currentTab = 'Metrics'; - updateActiveTab(); - $.get(url, function (data) { - if (data && data.status == "ok") { - _this.set('controller.model', data.metric) - _this.set('controller.detailview', true); - } - }); - } + /** + * Fetches the metric with the id specified in the route + * @param metric_id + * @return {Thenable|Promise} + */ + model({ metric_id }) { + const metricsUrl = `${metricsUrlRoot}/${metric_id}`; + return fetch(metricsUrl).then(response => response.json()); } }); diff --git a/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss b/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss index a02f187f54..27400773ef 100644 --- a/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss +++ b/wherehows-web/app/styles/components/dataset-compliance/_compliance-prompts.scss @@ -9,7 +9,6 @@ */ .ember-radio-button { display: inline-block; - width: 60px; cursor: pointer; } diff --git a/wherehows-web/app/styles/components/nacho/_nacho-filter-rail.scss b/wherehows-web/app/styles/components/nacho/_nacho-filter-rail.scss index e1c7dd8262..1a2980e840 100644 --- a/wherehows-web/app/styles/components/nacho/_nacho-filter-rail.scss +++ b/wherehows-web/app/styles/components/nacho/_nacho-filter-rail.scss @@ -78,7 +78,8 @@ $pad-width: 16px; .ember-radio-button { position: relative; left: 26px; - width: 60px; + min-width: 60px; + cursor: pointer; &:before, &:after { diff --git a/wherehows-web/app/styles/components/nacho/_nacho-table.scss b/wherehows-web/app/styles/components/nacho/_nacho-table.scss index 0c8c7df49b..dfc7aeff94 100644 --- a/wherehows-web/app/styles/components/nacho/_nacho-table.scss +++ b/wherehows-web/app/styles/components/nacho/_nacho-table.scss @@ -36,6 +36,9 @@ '& tr:nth-child(odd)': ( background-color: restyle-var(zebra) ) + ), + 'with variable width': ( + table-layout: auto ) ) )); @@ -50,4 +53,8 @@ &--stripped { @include restyle(table with stripped rows); } + + &--dynamic { + @include restyle(table with variable width); + } } diff --git a/wherehows-web/app/templates/components/browser/browser-view.hbs b/wherehows-web/app/templates/components/browser/browser-view.hbs index c1809bbbb7..fe96ccf92c 100644 --- a/wherehows-web/app/templates/components/browser/browser-view.hbs +++ b/wherehows-web/app/templates/components/browser/browser-view.hbs @@ -3,7 +3,7 @@ {{#each browseData as |entity|}} {{#nav-link "browse.entity" entity.entity - (query-params page=1 urn="") + (query-params page=1 urn="" name="") tagName="li" class="col-md-4 browse-nav__entity"}}
diff --git a/wherehows-web/app/templates/components/browser/browser-viewport.hbs b/wherehows-web/app/templates/components/browser/browser-viewport.hbs index 2e90ea55b9..4cc0dfd0c6 100644 --- a/wherehows-web/app/templates/components/browser/browser-viewport.hbs +++ b/wherehows-web/app/templates/components/browser/browser-viewport.hbs @@ -1,30 +1,102 @@ -{{#dataset-table - fields=entities as |table|}} - {{#table.body as |body|}} - {{#each - table.data as |entity|}} - {{#body.row as |row|}} - {{#row.cell}} - {{#link-to entityRoute entity.id}} - - {{entity.name}} - - {{/link-to}} +{{#if (eq currentEntity 'flows')}} + {{#dataset-table + fields=entities as |table|}} + {{#table.head as |head|}} + {{#head.column}} + Flow Group + {{/head.column}} - {{dataset-owner-list owners=entity.owners datasetName=entity.name}} + {{#head.column}} + Flow Name + {{/head.column}} - {{#if entity.formatedModified}} - Last Modified: + {{#head.column}} + Flow Level + {{/head.column}} - - {{moment-from-now entity.formatedModified }} - - {{/if}} - {{/row.cell}} - {{#row.cell}} - {{datasets/dataset-actions actionItems=actionItems}} - {{/row.cell}} - {{/body.row}} - {{/each}} - {{/table.body}} -{{/dataset-table}} + {{#head.column}} + Job Count + {{/head.column}} + + {{#head.column}} + Creation Time + {{/head.column}} + + {{#head.column}} + Modified Time + {{/head.column}} + {{/table.head}} + + {{#table.body as |body|}} + {{#each + table.data as |flow|}} + {{#body.row as |row|}} + {{#row.cell}} + {{flow.group}} + {{/row.cell}} + + {{#row.cell}} + {{#link-to entityRoute flow.id (query-params name=flow.appCode)}} + {{flow.name}} + {{/link-to}} + {{/row.cell}} + + {{#row.cell}} + {{flow.level}} + {{/row.cell}} + + {{#row.cell}} + {{flow.jobCount}} + {{/row.cell}} + + {{#row.cell}} + {{moment-calendar flow.created sameElse="MMM Do YYYY, h:mm a"}} + {{/row.cell}} + + {{#row.cell}} + {{moment-calendar flow.modified sameElse="MMM Do YYYY, h:mm a"}} + {{/row.cell}} + {{/body.row}} + {{/each}} + {{/table.body}} + {{/dataset-table}} +{{else}} + {{#dataset-table + fields=entities as |table|}} + {{#table.body as |body|}} + {{#each + table.data as |entity|}} + {{#body.row as |row|}} + {{#row.cell}} + {{#link-to entityRoute entity.id}} + + {{entity.name}} + + {{/link-to}} +
+ {{#if (eq currentEntity 'metrics')}} + {{entity.group}} - {{entity.dashboardName}} +
+ {{entity.description}} + {{/if}} + + {{#if (eq currentEntity 'datasests')}} + {{dataset-owner-list owners=entity.owners datasetName=entity.name}} + {{/if}} + + {{#if entity.formatedModified}} + Last Modified: + + + {{moment-from-now entity.formatedModified }} + + {{/if}} + {{/row.cell}} + {{#row.cell}} + {{datasets/dataset-actions actionItems=actionItems}} + {{/row.cell}} + {{/body.row}} + {{/each}} + {{/table.body}} + {{/dataset-table}} +{{/if}} diff --git a/wherehows-web/app/templates/components/metric-detail.hbs b/wherehows-web/app/templates/components/metric-detail.hbs index daf9969297..d41b4a36bf 100644 --- a/wherehows-web/app/templates/components/metric-detail.hbs +++ b/wherehows-web/app/templates/components/metric-detail.hbs @@ -1,238 +1,228 @@ -
-
+
+

{{ model.name }}

+
+
- + +
+ + + + + + + + + +
Dashboard Name + href="#" + data-name="dashboardName" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter dashboard name" + data-defaultValue="Please Input" + data-emptytext="Please Input" + data-value="{{model.dashboardName}}"> {{model.dashboardName}}
Metric Category + href="#" + data-name="category" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter metric category" + data-emptytext="Please Input"> {{model.category}}
Metric Group + href="#" + data-name="group" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter group" + data-emptytext="Please Input"> {{model.group}}
Metric Type + href="#" + data-name="refIDType" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter Type" + data-emptytext="Please Input" + data-value={{model.refIDType}}>
Metric Grain + href="#" + data-name="grain" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter grain" + data-emptytext="Please Input"> {{model.grain}}
Metric Formula {{ace-editor content=model.formula itemId=model.id savePath="/api/v1/metrics/{id}/update" saveParam="formula"}}
Metric Display Factor + href="#" + data-name="displayFactory" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter display factor" + data-emptytext="Please Input"> {{model.displayFactory}}
Metric Display Factor Sym + href="#" + data-name="displayFactorSym" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter display factor symbol" + data-emptytext="Please Input"> {{model.displayFactorSym}}
Metric Sub Category + href="#" + data-name="subCategory" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter sub category" + data-emptytext="Please Input"> {{model.subCategory}}
Metric Source + href="#" + data-name="source" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter source" + data-emptytext="Please Input" + data-value={{model.source}}>
Metric Source Type + href="#" + data-name="sourceType" + data-pk="{{model.id}}" + class="xeditable" + data-type="text" + data-placement="right" + data-title="Enter source type" + data-emptytext="Please Input" + data-value={{model.sourceType}}>
-
\ No newline at end of file +
diff --git a/wherehows-web/app/templates/datasets/dataset.hbs b/wherehows-web/app/templates/datasets/dataset.hbs index 7412d1f455..2fb0f2c32c 100644 --- a/wherehows-web/app/templates/datasets/dataset.hbs +++ b/wherehows-web/app/templates/datasets/dataset.hbs @@ -69,6 +69,7 @@
{{dataset-owner-list owners=owners datasetName=model.name}} + {{#if hasinstances}}
Instances: @@ -91,6 +92,7 @@
{{/if}} + {{#if hasversions}}
Versions: @@ -114,6 +116,7 @@
{{/if}} + +
{{#unless isPinot}}
diff --git a/wherehows-web/app/templates/flows.hbs b/wherehows-web/app/templates/flows.hbs index 9c9746fd40..88952f3e09 100644 --- a/wherehows-web/app/templates/flows.hbs +++ b/wherehows-web/app/templates/flows.hbs @@ -85,11 +85,7 @@
{{else}} -
-
-
- {{outlet}} -
-
+
+ {{outlet}}
-{{/if}} \ No newline at end of file +{{/if}} diff --git a/wherehows-web/app/templates/flows/flow.hbs b/wherehows-web/app/templates/flows/flow.hbs index c24cd68950..f80429ec3f 100644 --- a/wherehows-web/app/templates/flows/flow.hbs +++ b/wherehows-web/app/templates/flows/flow.hbs @@ -1 +1,51 @@ +
+ {{!--TODO: Make into Component--}} +
    + {{#each breadcrumbs as |crumb|}} +
  • + {{link-to crumb.crumb "browse.entity" "flows" (query-params page=1 urn=crumb.urn name=crumb.name) + class="nacho-breadcrumbs__crumb__grain"}} +
  • + {{/each}} +
+ + {{#dataset-table + fields=model.data.jobs as |table|}} + {{#table.head as |head|}} + {{#head.column columnName="name"}}Job Path{{/head.column}} + {{#head.column columnName="type"}}Job Type{{/head.column}} + {{#head.column}}Creation Time{{/head.column}} + {{#head.column}}Modified Time{{/head.column}} + {{/table.head}} + + {{#table.body as |body|}} + {{#each + (sort-by table.sortBy table.data) as |job|}} + {{#body.row as |row|}} + {{#row.cell}} + {{#if job.refFlowId}} + {{#link-to 'flows.flow' job.refFlowId (query-params name=model.data.name)}} + {{job.name}} + {{/link-to}} + {{else}} + {{job.name}} + {{/if}} + {{/row.cell}} + + {{#row.cell}} + {{job.type}} + {{/row.cell}} + + {{#row.cell}} + {{moment-calendar job.created sameElse="MMM Do YYYY, h:mm a"}} + {{/row.cell}} + + {{#row.cell}} + {{moment-calendar job.modified sameElse="MMM Do YYYY, h:mm a"}} + {{/row.cell}} + {{/body.row}} + {{/each}} + {{/table.body}} + {{/dataset-table}} +
{{outlet}} diff --git a/wherehows-web/app/templates/metrics/metric.hbs b/wherehows-web/app/templates/metrics/metric.hbs index c24cd68950..f70af4703f 100644 --- a/wherehows-web/app/templates/metrics/metric.hbs +++ b/wherehows-web/app/templates/metrics/metric.hbs @@ -1 +1,13 @@ -{{outlet}} +
+ {{!--TODO: Make into Component--}} +
    + {{#each breadcrumbs as |crumb|}} +
  • + {{link-to crumb.crumb "browse.entity" "metrics" (query-params page=1 name=crumb.name) + class="nacho-breadcrumbs__crumb__grain"}} +
  • + {{/each}} +
+ + {{metric-detail showLineage=showLineage model=model}} +
diff --git a/wherehows-web/app/utils/validators/urn.js b/wherehows-web/app/utils/validators/urn.js index e3cc93ce42..f80064d998 100644 --- a/wherehows-web/app/utils/validators/urn.js +++ b/wherehows-web/app/utils/validators/urn.js @@ -5,10 +5,16 @@ * @type {RegExp} */ const urnRegex = /([a-z_]+):\/{3}([a-z0-9_\-\/\{\}]*)/i; + +/** + * Matches urn's that occur in flow urls + * @type {RegExp} + */ +const specialFlowUrnRegex = /(?:\?urn=)([a-z0-9_\-\/{}\s]+)/i; /** * Asserts that a provided string matches the urn pattern above * @param {String} candidateUrn the string to test on */ export default candidateUrn => urnRegex.test(String(candidateUrn)); -export { urnRegex }; +export { urnRegex, specialFlowUrnRegex }; diff --git a/wherehows-web/ember-cli-build.js b/wherehows-web/ember-cli-build.js index c1edbe1894..969c11a6ef 100644 --- a/wherehows-web/ember-cli-build.js +++ b/wherehows-web/ember-cli-build.js @@ -96,6 +96,9 @@ module.exports = function(defaults) { app.import( 'bower_components/jsondiffpatch/public/formatters-styles/annotated.css' ); + app.import( + 'bower_components/x-editable/dist/bootstrap3-editable/css/bootstrap-editable.css' + ); app.import('vendor/legacy_styles/main.css'); app.import('vendor/legacy_styles/comments.css'); app.import('vendor/legacy_styles/wherehows.css'); @@ -129,9 +132,9 @@ module.exports = function(defaults) { app.import('vendor/CsvToMarkdown.js'); app.import('vendor/typeahead.jquery.js'); app.import('bower_components/marked/marked.min.js'); - app.import('bower_components/ace-builds/src-min/ace.js'); - app.import('bower_components/ace-builds/src-min/theme-github.js'); - app.import('bower_components/ace-builds/src-min/mode-sql.js'); + app.import('bower_components/ace-builds/src-noconflict/ace.js'); + app.import('bower_components/ace-builds/src-noconflict/theme-github.js'); + app.import('bower_components/ace-builds/src-noconflict/mode-sql.js'); app.import('bower_components/toastr/toastr.min.js'); app.import('bower_components/highcharts/highcharts.js'); app.import( @@ -140,6 +143,9 @@ module.exports = function(defaults) { app.import( 'bower_components/jsondiffpatch/public/build/jsondiffpatch-formatters.min.js' ); + app.import( + 'bower_components/x-editable/dist/bootstrap3-editable/js/bootstrap-editable.min.js' + ); return app.toTree(new MergeTrees([faFontTree, bsFontTree, treegridImgTree])); }; diff --git a/wherehows-web/tests/unit/controllers/flows/flow-test.js b/wherehows-web/tests/unit/controllers/flows/flow-test.js new file mode 100644 index 0000000000..6487fa13e7 --- /dev/null +++ b/wherehows-web/tests/unit/controllers/flows/flow-test.js @@ -0,0 +1,12 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('controller:flows/flow', 'Unit | Controller | flows/flow', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +// Replace this with your real tests. +test('it exists', function(assert) { + let controller = this.subject(); + assert.ok(controller); +});