adds browse feature for flows and metrics

This commit is contained in:
Seyi Adebajo 2017-05-15 12:45:28 -07:00 committed by Mars Lan
parent 5dede95d9d
commit a9c622c832
36 changed files with 967 additions and 436 deletions

View File

@ -32,11 +32,11 @@ const asyncRequestBrowseData = (page, entity, urls) =>
try { try {
const thunks = await Promise.all([ const thunks = await Promise.all([
dispatch(lazyRequestPagedFlows({ baseURL: urls.flows, page })), dispatch(lazyRequestPagedFlows({ baseURL: urls.flows, query: { page } })),
dispatch(lazyRequestPagedMetrics({ baseURL: urls.metrics, page })), dispatch(lazyRequestPagedMetrics({ baseURL: urls.metrics, query: { page } })),
dispatch(lazyRequestPagedDatasets({ baseURL: urls.datasets, 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 * Check that none of the actions has an error flag in FSA

View File

@ -3,6 +3,8 @@ import { createAction } from 'redux-actions';
import actionSet from 'wherehows-web/actions/action-set'; import actionSet from 'wherehows-web/actions/action-set';
import { lazyRequestUrnPagedDatasets, lazyRequestDatasetNodes } from 'wherehows-web/actions/datasets'; 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; const { debug } = Ember;
@ -23,29 +25,41 @@ const receiveNodeList = createAction(ActionTypes.RECEIVE_NODE_LIST);
* @param {String} listURL * @param {String} listURL
* @param {Array} queryParams current list of query parameters for the Ember route * @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 * Async thunk
* @param {Function} dispatch * @param {Function} dispatch
* @return {Promise.<*>} * @return {Promise.<*>}
*/ */
async function(dispatch) { async function(dispatch) {
const { entity, page, urn } = params; const { entity, page, urn, name } = params;
const query = { page, urn }; // Extract relevant query parameters into query object
const query = { page, urn, name };
dispatch(requestNodeList({ entity, listURL, query, queryParams })); dispatch(requestNodeList({ entity, listURL, query, queryParams }));
// For each entity fetch the list of nodes and the actual entities for the given query
try { try {
let nodesResult = {}, pagedEntities = {}; let nodesResult = {}, pagedEntities = {};
switch (entity) { switch (entity) {
case 'datasets': case 'datasets':
[nodesResult, pagedEntities] = await [ [nodesResult, pagedEntities] = await Promise.all([
dispatch(lazyRequestDatasetNodes({ listURL, query })), dispatch(lazyRequestDatasetNodes({ listURL, query })),
dispatch(lazyRequestUrnPagedDatasets({ query })) dispatch(lazyRequestUrnPagedDatasets({ query }))
]; ]);
break; break;
case 'metrics': case 'metrics':
[nodesResult, pagedEntities] = await Promise.all([
dispatch(lazyRequestMetricNodes({ listURL, query })),
dispatch(lazyRequestNamedPagedMetrics({ query }))
]);
break;
case 'flows': case 'flows':
[nodesResult, pagedEntities] = await Promise.all([
dispatch(lazyRequestFlowsNodes({ listURL, query })),
dispatch(lazyRequestPagedUrnApplicationFlows({ query }))
]);
break;
default: default:
return; return;
} }
@ -66,4 +80,4 @@ const asyncRequestNodeList = (params, listURL, { queryParams }) =>
} }
}; };
export { ActionTypes, asyncRequestNodeList }; export { ActionTypes, asyncRequestEntityQueryData };

View File

@ -40,10 +40,10 @@ const createAsyncThunk = (
* @return {Promise.<*>} * @return {Promise.<*>}
*/ */
) => async (dispatch, getState) => { ) => async (dispatch, getState) => {
const { status = 'error', data } = await asyncExecutor(getState); const response = await asyncExecutor(getState);
if (status === 'ok') { if (response.status === 'ok') {
return dispatch(receiverActionCreator({ data })); return dispatch(receiverActionCreator(response));
} }
return dispatch(receiverActionCreator(new Error(`Request failed with status ${status}`))); return dispatch(receiverActionCreator(new Error(`Request failed with status ${status}`)));

View File

@ -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 * Request urn child nodes/ datasets for the specified entity
* @param entity * @param entity
@ -75,7 +148,56 @@ const fetchNodes = entity => getState => {
}, `${listURL}/${entity}`); }, `${listURL}/${entity}`);
// TODO: DSS-7019 remove any parsing from response objects. in createLazyRequest and update all call sites // 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
};

View File

@ -1,2 +1,2 @@
export * from 'wherehows-web/actions/entities/entities'; 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';

View File

@ -1,5 +1,10 @@
import { createAction } from 'redux-actions'; 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'; import actionSet from 'wherehows-web/actions/action-set';
/** /**
@ -9,7 +14,13 @@ import actionSet from 'wherehows-web/actions/action-set';
const ActionTypes = { const ActionTypes = {
REQUEST_PAGED_FLOWS: actionSet('REQUEST_PAGED_FLOWS'), REQUEST_PAGED_FLOWS: actionSet('REQUEST_PAGED_FLOWS'),
SELECT_PAGED_FLOWS: actionSet('SELECT_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); const requestPagedFlows = createAction(ActionTypes.REQUEST_PAGED_FLOWS);
@ -21,7 +32,28 @@ const receivePagedFlows = createAction(
() => ({ receivedAt: Date.now() }) () => ({ 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 // 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')); 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 };

View File

@ -1,5 +1,10 @@
import { createAction } from 'redux-actions'; 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'; import actionSet from 'wherehows-web/actions/action-set';
/** /**
@ -9,7 +14,13 @@ import actionSet from 'wherehows-web/actions/action-set';
const ActionTypes = { const ActionTypes = {
REQUEST_PAGED_METRICS: actionSet('REQUEST_PAGED_METRICS'), REQUEST_PAGED_METRICS: actionSet('REQUEST_PAGED_METRICS'),
SELECT_PAGED_METRICS: actionSet('SELECT_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); const requestPagedMetrics = createAction(ActionTypes.REQUEST_PAGED_METRICS);
@ -23,10 +34,47 @@ const receivePagedMetrics = createAction(
() => ({ receivedAt: Date.now() }) () => ({ 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 // 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 // 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
};

View File

@ -44,15 +44,11 @@ export default Ember.Component.extend({
var url = this.get('savePath'); var url = this.get('savePath');
url = url.replace(/\{.\w+\}/, this.get('itemId')) url = url.replace(/\{.\w+\}/, this.get('itemId'))
var method = 'POST'; var method = 'POST';
var token = $("#csrfToken").val().replace('/', ''); var data = {};
var data = {"csrfToken": token};
data[this.get('saveParam')] = this.editor.getSession().getValue() data[this.get('saveParam')] = this.editor.getSession().getValue()
$.ajax({ $.ajax({
url: url, url: url,
method: method, method: method,
headers: {
'Csrf-Token': token
},
dataType: 'json', dataType: 'json',
data: data data: data
}).done(function (data, txt, xhr) { }).done(function (data, txt, xhr) {

View File

@ -1,6 +1,6 @@
import Ember from 'ember'; import Ember from 'ember';
import connect from 'ember-redux/components/connect'; 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; const { Component } = Ember;
@ -10,13 +10,7 @@ const { Component } = Ember;
* @type {RegExp} * @type {RegExp}
*/ */
const pageRegex = /\/page\/([0-9]+)/i; const pageRegex = /\/page\/([0-9]+)/i;
/** const nameRegex = /\/name\/([0-9a-z()_{}\[\]\/\s]+)/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]+)\/?/;
/** /**
* Takes a node url and parses out the query params and path spec to be included in the link component * 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 nodeUrlToQueryParams = nodeUrl => {
const pageMatch = nodeUrl.match(pageRegex); const pageMatch = nodeUrl.match(pageRegex);
const urnMatch = nodeUrl.match(urnRegex); const urnMatch = nodeUrl.match(urnRegex);
const flowUrnMatch = nodeUrl.match(specialFlowUrnRegex);
const nameMatch = nodeUrl.match(nameRegex);
let queryParams = null; let queryParams = null;
// If we have a page match, append the page number to eventual urn object // 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 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, { queryParams = Object.assign({}, queryParams, {
// Extract the entire match as urn value // Extract the entire match as urn value
urn: urnMatch[0] urn: urn[0]
}); });
} }
return queryParams; 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 * Selector function that takes a Redux Store to extract
* state props for the browser-rail * state props for the browser-rail
@ -58,36 +78,52 @@ const stateToComputed = state => {
// Extracts the current entity active in the browse view // Extracts the current entity active in the browse view
const { browseEntity: { currentEntity = '' } = {} } = state; const { browseEntity: { currentEntity = '' } = {} } = state;
// Retrieves properties for the current entity from the state tree // 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 // 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 // returned entities contain id prop that is the singular type name, suffixed with Id, e.g. metricId
// datasets -> dataset, metrics -> metric, flows -> flow // datasets -> dataset, metrics -> metric, flows -> flow
const singularName = currentEntity.slice(0, -1); 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 * Creates dynamic query link params for each node
* @type {Array} list of child nodes or datasets to render * @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); 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 the id prop (datasetId|metricId|flowId) is truthy, then it is a standalone entity
if (id) { if (id) {
return { let idNode = Object.assign({}, node);
title: nodeName,
text: nodeName, if (singularName === 'flow' && application) {
idNode = Object.assign({}, idNode, {
queryParams: {
name: application
}
});
}
return Object.assign({}, idNode, {
route: `${currentEntity}.${singularName}`, route: `${currentEntity}.${singularName}`,
model: nodeUrl.match(entityRegex)[1] model: id
}; });
} }
return { return Object.assign({}, node, {
title: nodeName,
text: nodeName,
route: `browse.entity`, route: `browse.entity`,
model: currentEntity, model: currentEntity,
queryParams: nodeUrlToQueryParams(nodeUrl) queryParams: nodeUrlToQueryParams(nodeUrl)
}; });
}); });
return { nodes }; return { nodes };

View File

@ -3,6 +3,25 @@ import connect from 'ember-redux/components/connect';
const { Component } = Ember; 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 * Selector function that takes a Redux Store to extract
* state props for the browser-rail * state props for the browser-rail
@ -12,12 +31,17 @@ const stateToComputed = state => {
// Extracts the current entity active in the browse view // Extracts the current entity active in the browse view
const { browseEntity: { currentEntity = '' } = {} } = state; const { browseEntity: { currentEntity = '' } = {} } = state;
// Retrieves properties for the current entity from the state tree // 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 // 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 // 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 // Read the list of entities, stored in the byId property
const { [currentEntity]: { byId: entities } } = state; const { [currentEntity]: { byId: entities } } = state;
/** /**

View File

@ -41,13 +41,13 @@ const datasetClassifiersKeys = Object.keys(datasetClassifiers);
// TODO: DSS-6671 Extract to constants module // TODO: DSS-6671 Extract to constants module
const successUpdating = 'Your changes have been successfully saved!'; const successUpdating = 'Your changes have been successfully saved!';
const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.'; const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.';
const missingTypes = 'Looks like some fields are marked as `Confidential` or ' + const missingTypes =
'`Highly Confidential` but do not have a specified `Field Format`?'; 'Looks like some fields are marked as `Confidential` or `Highly Confidential` but do not have a specified `Field Format`?';
const hiddenTrackingFieldsMsg = htmlSafe( const hiddenTrackingFieldsMsg = htmlSafe(
'<p>Hey! Just a heads up that some fields in this dataset have been hidden from the table(s) below. ' + '<p>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.</p>' + "These are tracking fields for which we've been able to predetermine the compliance classification.</p>" +
'<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' + '<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' +
'Hopefully, this saves you some scrolling!</p>' 'Hopefully, this saves you some scrolling!</p>'
); );
/** /**
@ -55,8 +55,7 @@ const hiddenTrackingFieldsMsg = htmlSafe(
* for now, so no need to make into a helper * for now, so no need to make into a helper
* @param {String} string * @param {String} string
*/ */
const formatAsCapitalizedStringWithSpaces = string => const formatAsCapitalizedStringWithSpaces = string => string.replace(/[A-Z]/g, match => ` ${match}`).capitalize();
string.replace(/[A-Z]/g, match => ` ${match}`).capitalize();
export default Component.extend({ export default Component.extend({
sortColumnWithName: 'identifierField', sortColumnWithName: 'identifierField',
@ -73,10 +72,9 @@ export default Component.extend({
// Map logicalTypes to options better consumed by drop down // Map logicalTypes to options better consumed by drop down
logicalTypes: ['', ...logicalTypes.sort()].map(value => { logicalTypes: ['', ...logicalTypes.sort()].map(value => {
const label = value ? const label = value
value.replace(/_/g, ' ') ? value.replace(/_/g, ' ').replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize())
.replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize()) : : 'Not Specified';
'Not Specified';
return { return {
value, value,
@ -122,25 +120,26 @@ export default Component.extend({
*/ */
fieldNameToClass: computed( fieldNameToClass: computed(
`${sourceClassificationKey}.{confidential,limitedDistribution,highlyConfidential}.[]`, `${sourceClassificationKey}.{confidential,limitedDistribution,highlyConfidential}.[]`,
function () { function() {
const sourceClasses = getWithDefault(this, sourceClassificationKey, []); const sourceClasses = getWithDefault(this, sourceClassificationKey, []);
// Creates a lookup table of fieldNames to classification // Creates a lookup table of fieldNames to classification
// Also, the expectation is that the association from fieldName -> classification // Also, the expectation is that the association from fieldName -> classification
// is one-to-one hence no check to ensure a fieldName gets clobbered // is one-to-one hence no check to ensure a fieldName gets clobbered
// in the lookup assignment // in the lookup assignment
return Object.keys(sourceClasses) return Object.keys(sourceClasses).reduce(
.reduce((lookup, classificationKey) => (lookup, classificationKey) =>
// For the provided classificationKey, iterate over it's fieldNames, // For the provided classificationKey, iterate over it's fieldNames,
// and assign the classificationKey to the fieldName in the table // and assign the classificationKey to the fieldName in the table
(sourceClasses[classificationKey] || []).reduce((lookup, field) => { (sourceClasses[classificationKey] || []).reduce((lookup, field) => {
const { identifierField } = field; const { identifierField } = field;
// cKey -> 1...fieldNameList => fieldName -> cKey // cKey -> 1...fieldNameList => fieldName -> cKey
lookup[identifierField] = classificationKey; lookup[identifierField] = classificationKey;
return lookup; return lookup;
}, lookup), }, lookup),
{} {}
); );
}), }
),
/** /**
* Lists all the dataset fields found in the `columns` api, and intersects * 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 // assign to field, otherwise null
// Rather than assigning the default classification here, nulling gives the benefit of allowing // 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 // subsequent consumer know that this field did not have a previous classification
const field = classification ? const field = classification
get(this, `${sourceClassificationKey}.${classification}`) ? get(this, `${sourceClassificationKey}.${classification}`).findBy('identifierField', identifierField)
.findBy('identifierField', identifierField) : : null;
null;
// Extract the logicalType from the field // Extract the logicalType from the field
const logicalType = isPresent(field) ? field.logicalType : null; const logicalType = isPresent(field) ? field.logicalType : null;
@ -190,21 +188,22 @@ export default Component.extend({
* tracking header. * tracking header.
* Used to indicate to viewer that these fields are hidden. * Used to indicate to viewer that these fields are hidden.
*/ */
containsHiddenTrackingFields: computed( containsHiddenTrackingFields: computed('classificationDataFieldsSansHiddenTracking.length', function() {
'classificationDataFieldsSansHiddenTracking.length', // If their is a diff in complianceDataFields and complianceDataFieldsSansHiddenTracking,
function () { // then we have hidden tracking fields
// If their is a diff in complianceDataFields and complianceDataFieldsSansHiddenTracking, return (
// then we have hidden tracking fields get(this, 'classificationDataFieldsSansHiddenTracking.length') !== get(this, 'classificationDataFields.length')
return get(this, 'classificationDataFieldsSansHiddenTracking.length') !== get(this, 'classificationDataFields.length'); );
}), }),
/** /**
* @type {Array.<Object>} Filters the mapped confidential data fields without `kafka type` * @type {Array.<Object>} Filters the mapped confidential data fields without `kafka type`
* tracking headers * tracking headers
*/ */
classificationDataFieldsSansHiddenTracking: computed('classificationDataFields.[]', function () { classificationDataFieldsSansHiddenTracking: computed('classificationDataFields.[]', function() {
return get(this, 'classificationDataFields') return get(this, 'classificationDataFields').filter(
.filter(({ identifierField }) => !isTrackingHeaderField(identifierField)); ({ identifierField }) => !isTrackingHeaderField(identifierField)
);
}), }),
/** /**
@ -219,20 +218,19 @@ export default Component.extend({
.then(({ status = 'error' }) => { .then(({ status = 'error' }) => {
// The server api currently responds with an object containing // The server api currently responds with an object containing
// a status when complete // a status when complete
return status === 'ok' ? return status === 'ok'
setProperties(this, { ? setProperties(this, {
_message: successMessage || successUpdating, _message: successMessage || successUpdating,
_alertType: 'success' _alertType: 'success'
}) : })
Promise.reject(new Error(`Reason code for this is ${status}`)); : Promise.reject(new Error(`Reason code for this is ${status}`));
}) })
.catch(err => { .catch(err => {
let _message = `${failedUpdating} \n ${err}`; let _message = `${failedUpdating} \n ${err}`;
let _alertType = 'danger'; let _alertType = 'danger';
if (get(this, 'isNewSecuritySpecification')) { if (get(this, 'isNewSecuritySpecification')) {
_message = 'This dataset does not have any ' + _message = 'This dataset does not have any previously saved fields with a Security Classification.';
'previously saved fields with a Security Classification.';
_alertType = 'info'; _alertType = 'info';
} }
@ -264,7 +262,11 @@ export default Component.extend({
const nextProps = { identifierField, logicalType }; const nextProps = { identifierField, logicalType };
// The current classification name for the candidate identifier // The current classification name for the candidate identifier
const currentClassLookup = get(this, 'fieldNameToClass'); const currentClassLookup = get(this, 'fieldNameToClass');
const defaultClassification = getWithDefault(this, `${sourceClassificationKey}.${defaultFieldDataTypeClassification[logicalType]}`, []); const defaultClassification = getWithDefault(
this,
`${sourceClassificationKey}.${defaultFieldDataTypeClassification[logicalType]}`,
[]
);
let currentClassificationName = currentClassLookup[identifierField]; let currentClassificationName = currentClassLookup[identifierField];
/** /**
@ -278,11 +280,11 @@ export default Component.extend({
const currentClassification = get(this, `${sourceClassificationKey}.${currentClassificationName}`); const currentClassification = get(this, `${sourceClassificationKey}.${currentClassificationName}`);
if (!Array.isArray(currentClassification)) { if (!Array.isArray(currentClassification)) {
throw new Error(` throw new Error(
You have specified a classification object that is not a list ${currentClassification}. `You have specified a classification object that is not a list ${currentClassification}.
Ensure that the classification for this identifierField (${identifierField}) is 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); const field = currentClassification.findBy('identifierField', identifierField);
@ -292,8 +294,7 @@ export default Component.extend({
if (isPresent(field)) { if (isPresent(field)) {
// Remove identifierField from list // Remove identifierField from list
currentClassification.setObjects( currentClassification.setObjects(
currentClassification.filter( currentClassification.filter(({ identifierField: fieldName }) => fieldName !== identifierField)
({ identifierField: fieldName }) => fieldName !== identifierField)
); );
} }
@ -343,24 +344,17 @@ export default Component.extend({
// in any other classification lists by checking that the lookup is void // in any other classification lists by checking that the lookup is void
if (!isBlank(currentClass)) { if (!isBlank(currentClass)) {
// Get the current classification list // Get the current classification list
const currentClassification = get( const currentClassification = get(this, `${sourceClassificationKey}.${currentClass}`);
this,
`${sourceClassificationKey}.${currentClass}`
);
// Remove identifierField from list // Remove identifierField from list
currentClassification.setObjects( currentClassification.setObjects(
currentClassification.filter( currentClassification.filter(({ identifierField: fieldName }) => fieldName !== identifierField)
({ identifierField: fieldName }) => fieldName !== identifierField)
); );
} }
if (classKey) { if (classKey) {
// Get the candidate list // Get the candidate list
let classification = get( let classification = get(this, `${sourceClassificationKey}.${classKey}`);
this,
`${sourceClassificationKey}.${classKey}`
);
// In the case that the list is not pre-populated, // In the case that the list is not pre-populated,
// the value will be the default null, array ops won't work here // the value will be the default null, array ops won't work here
// ...so make array // ...so make array
@ -400,9 +394,8 @@ export default Component.extend({
* @type {Boolean} * @type {Boolean}
*/ */
const classedFieldsHaveLogicalType = classifiers.every(classifier => const classedFieldsHaveLogicalType = classifiers.every(classifier =>
this.ensureFieldsContainLogicalType( this.ensureFieldsContainLogicalType(getWithDefault(this, `${sourceClassificationKey}.${classifier}`, []))
getWithDefault(this, `${sourceClassificationKey}.${classifier}`, []) );
));
if (classedFieldsHaveLogicalType) { if (classedFieldsHaveLogicalType) {
this.whenRequestCompletes(get(this, 'onSave')()); this.whenRequestCompletes(get(this, 'onSave')());

View File

@ -1,4 +1,22 @@
import Ember from 'ember'; 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
);
}
}
}); });

View File

@ -23,7 +23,7 @@ export default Ember.Component.extend({
} }
}).done(function (data, txt, xhr) { }).done(function (data, txt, xhr) {
_this.set('metric.watchId', data.watchId) _this.set('metric.watchId', data.watchId)
_this.sendAction('getMetrics') // _this.sendAction('getMetrics')
}).fail(function (xhr, txt, err) { }).fail(function (xhr, txt, err) {
console.log('Error: Could not watch metric.') console.log('Error: Could not watch metric.')
}) })

View File

@ -5,7 +5,9 @@ const { Controller } = Ember;
* Handles query params for browse.entity route * Handles query params for browse.entity route
*/ */
export default Controller.extend({ export default Controller.extend({
queryParams: ['page', 'urn'], queryParams: ['page', 'urn', 'size', 'name'],
page: 1, page: 1,
urn: '' urn: '',
name: '',
size: 10
}); });

View File

@ -0,0 +1,6 @@
import Ember from 'ember';
const { Controller } = Ember;
export default Controller.extend({
queryParams: ['name']
});

View File

@ -1,12 +1,20 @@
import { import {
initializeState, initializeState,
receiveNodes, urnsToNodeUrn,
receiveEntities, receiveEntities,
createUrnMapping, createUrnMapping,
createPageMapping createPageMapping
} from 'wherehows-web/reducers/entities'; } from 'wherehows-web/reducers/entities';
import { ActionTypes } from 'wherehows-web/actions/datasets'; 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 * datasets root reducer
* Takes the `datasets` slice of the state tree and performs the specified reductions for each action * 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 * @prop {String} action.type actionType
* @return {Object} * @return {Object}
*/ */
export default (state = initializeState(), action = {}) => { export default (state = initialState, action = {}) => {
switch (action.type) { switch (action.type) {
// Action indicating a request for datasets by page // Action indicating a request for datasets by page
case ActionTypes.SELECT_PAGED_DATASETS: case ActionTypes.SELECT_PAGED_DATASETS:
case ActionTypes.REQUEST_PAGED_DATASETS: case ActionTypes.REQUEST_PAGED_DATASETS:
return Object.assign({}, state, { return Object.assign({}, state, {
query: Object.assign({}, state.query, { query: Object.assign({}, state.query, {
page: action.payload.page page: action.payload.query.page
}), }),
baseURL: action.payload.baseURL || state.baseURL, baseURL: action.payload.baseURL || state.baseURL,
isFetching: true 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 case ActionTypes.RECEIVE_DATASET_NODES: // Action indicating a receipt of list nodes / datasets for dataset urn
return Object.assign({}, state, { return Object.assign({}, state, {
isFetching: false, isFetching: false,
nodesByUrn: receiveNodes(state, action.payload) nodesByUrn: urnsToNodeUrn(state, action.payload)
}); });
default: default:

View File

@ -52,6 +52,15 @@ const appendUrnIdMap = (
[parentUrn]: union(urnEntities[parentUrn], entities.mapBy('id')) [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 * Returns a curried function that receives entityName to lookup on the props object
* @param {String} entityName * @param {String} entityName
@ -79,12 +88,27 @@ const entitiesToPage = (
* Maps a urn to a list of child nodes from the list api * Maps a urn to a list of child nodes from the list api
* @param {Object} state * @param {Object} state
* @param {Array} nodes * @param {Array} nodes
* @return {Object} * @param {String} parentUrn
* @return {*}
*/ */
const urnsToNodeUrn = (state, { data: nodes = [] }) => { const urnsToNodeUrn = (state, { nodes = [], parentUrn = null } = {}) => {
const { query: { urn }, nodesByUrn } = state; const { nodesByUrn } = state;
return Object.assign({}, nodesByUrn, { 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 * @param {String} entityName
*/ */
const createPageMapping = entityName => (state, payload = {}) => { const createPageMapping = entityName => (state, payload = {}) => {
return entitiesToPage(entityName)(state, payload); return entitiesToPage(entityName)(state, payload);
}; };
/** export {
* Takes the response from the list api request and invokes a function to initializeState,
* map a urn to child urns or nodes namesToNodeName,
* @param {Object} state urnsToNodeUrn,
* @param {Object} payload the response from the list endpoint/api receiveEntities,
* @return {Object} createUrnMapping,
*/ createPageMapping,
const receiveNodes = (state, payload = {}) => urnsToNodeUrn(state, payload); createNameMapping
};
export { receiveNodes, initializeState, receiveEntities, createUrnMapping, createPageMapping };

View File

@ -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'; 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 * 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 * @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 * @prop {String} action.type actionType
* @return {Object} * @return {Object}
*/ */
export default (state = initializeState(), action = {}) => { export default (state = initialState, action = {}) => {
switch (action.type) { switch (action.type) {
case ActionTypes.RECEIVE_PAGED_FLOWS: case ActionTypes.RECEIVE_PAGED_FLOWS:
case ActionTypes.RECEIVE_PAGED_URN_FLOWS:
return Object.assign({}, state, { return Object.assign({}, state, {
isFetching: false, isFetching: false,
byUrn: createUrnMapping('flows')(state.byUrn, action.payload),
byId: receiveEntities('flows')(state.byId, 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.SELECT_PAGED_FLOWS:
case ActionTypes.REQUEST_PAGED_FLOWS: case ActionTypes.REQUEST_PAGED_FLOWS:
return Object.assign({}, state, { return Object.assign({}, state, {
query: Object.assign({}, state.query, { 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 isFetching: true
}); });

View File

@ -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'; import { ActionTypes } from 'wherehows-web/actions/metrics';
/** /**
* Sets the default initial state for metrics slice. Appends a byName property to the shared representation. * Sets the default initial state for metrics slice. Appends a byName property to the shared representation.
* @type {Object} * @type {Object}
*/ */
const initialState = Object.assign({}, initializeState(), { const initialState = Object.assign({}, initializeState(), {
byName: {} byName: {},
nodesByName: []
}); });
/** /**
@ -23,19 +29,36 @@ export default (state = initialState, action = {}) => {
case ActionTypes.SELECT_PAGED_METRICS: case ActionTypes.SELECT_PAGED_METRICS:
case ActionTypes.REQUEST_PAGED_METRICS: case ActionTypes.REQUEST_PAGED_METRICS:
return Object.assign({}, state, { return Object.assign({}, state, {
query: Object.assign({}, state.query, { query: Object.assign({}, state.query, action.payload.query),
page: action.payload.page
}),
baseURL: action.payload.baseURL || state.baseURL, baseURL: action.payload.baseURL || state.baseURL,
isFetching: true isFetching: true
}); });
// Action indicating a receipt of metrics by page // 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_METRICS:
case ActionTypes.RECEIVE_PAGED_NAMED_METRICS:
return Object.assign({}, state, { return Object.assign({}, state, {
isFetching: false, isFetching: false,
byUrn: createUrnMapping('metrics')(state.byUrn, action.payload),
byId: receiveEntities('metrics')(state.byId, 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: default:

View File

@ -8,9 +8,9 @@ const { Route } = Ember;
// TODO: Route should transition to browse/entity, pay attention to the fact that // TODO: Route should transition to browse/entity, pay attention to the fact that
// this route initializes store with entity metrics on entry // this route initializes store with entity metrics on entry
const entityUrls = { const entityUrls = {
datasets: '/api/v1/datasets?size=10', datasets: '/api/v1/datasets',
metrics: '/api/v1/metrics?size=10', metrics: '/api/v1/metrics',
flows: '/api/v1/flows?size=10' flows: '/api/v1/flows'
}; };
export default route({ export default route({

View File

@ -1,15 +1,21 @@
import Ember from 'ember'; import Ember from 'ember';
import route from 'ember-redux/route'; import route from 'ember-redux/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; 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; const { Route } = Ember;
// TODO: DSS-6581 Create URL retrieval module
const listUrl = '/api/v1/list'; 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, { const BrowseEntityRoute = Route.extend(AuthenticatedRouteMixin, {
queryParams: queryParams.reduce( queryParams: queryParamsKeys.reduce(
(queryParams, param) => (queryParams, param) =>
Object.assign({}, queryParams, { Object.assign({}, queryParams, {
[param]: { refreshModel: true } [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({ export default route({
model: (dispatch, params) => dispatch(asyncRequestNodeList(params, listUrl, { queryParams })) /**
*
* @param dispatch
* @param params
*/
model: (dispatch, params) => dispatch(asyncRequestEntityQueryData(params, listUrl, { queryParamsKeys }))
})(BrowseEntityRoute); })(BrowseEntityRoute);

View File

@ -1,4 +1,44 @@
import Ember from 'ember'; 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;
}
}); });

View File

@ -1,111 +1,48 @@
import Ember from 'ember'; import Ember from 'ember';
import fetch from 'ember-network/fetch';
export default Ember.Route.extend({ const { Route, setProperties } = Ember;
setupController: function (controller, model) {
const metricsController = this.controllerFor('metrics');
if (metricsController) { const metricsUrlRoot = '/api/v1/metrics';
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);
}
/**
* 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; return [
$.get(listUrl, function (data) { { crumb: 'Metrics', name: '' },
if (data && data.status == "ok") { { crumb: dashboardName, name: dashboardName },
// renderMetricListView(data.nodes, id); { 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") { * Fetches the metric with the id specified in the route
// renderMetricListView(data.nodes, id); * @param metric_id
} * @return {Thenable<V, void>|Promise<V, X>}
}); */
model({ metric_id }) {
var url = 'api/v1/metrics/' + this.get('controller.model.id') const metricsUrl = `${metricsUrlRoot}/${metric_id}`;
var _this = this return fetch(metricsUrl).then(response => response.json());
currentTab = 'Metrics';
updateActiveTab();
$.get(url, function (data) {
if (data && data.status == "ok") {
_this.set('controller.model', data.metric)
_this.set('controller.detailview', true);
}
});
}
} }
}); });

View File

@ -9,7 +9,6 @@
*/ */
.ember-radio-button { .ember-radio-button {
display: inline-block; display: inline-block;
width: 60px;
cursor: pointer; cursor: pointer;
} }

View File

@ -78,7 +78,8 @@ $pad-width: 16px;
.ember-radio-button { .ember-radio-button {
position: relative; position: relative;
left: 26px; left: 26px;
width: 60px; min-width: 60px;
cursor: pointer;
&:before, &:before,
&:after { &:after {

View File

@ -36,6 +36,9 @@
'& tr:nth-child(odd)': ( '& tr:nth-child(odd)': (
background-color: restyle-var(zebra) background-color: restyle-var(zebra)
) )
),
'with variable width': (
table-layout: auto
) )
) )
)); ));
@ -50,4 +53,8 @@
&--stripped { &--stripped {
@include restyle(table with stripped rows); @include restyle(table with stripped rows);
} }
&--dynamic {
@include restyle(table with variable width);
}
} }

View File

@ -3,7 +3,7 @@
{{#each browseData as |entity|}} {{#each browseData as |entity|}}
{{#nav-link {{#nav-link
"browse.entity" entity.entity "browse.entity" entity.entity
(query-params page=1 urn="") (query-params page=1 urn="" name="")
tagName="li" tagName="li"
class="col-md-4 browse-nav__entity"}} class="col-md-4 browse-nav__entity"}}
<div class="browse-nav__item"> <div class="browse-nav__item">

View File

@ -1,30 +1,102 @@
{{#dataset-table {{#if (eq currentEntity 'flows')}}
fields=entities as |table|}} {{#dataset-table
{{#table.body as |body|}} fields=entities as |table|}}
{{#each {{#table.head as |head|}}
table.data as |entity|}} {{#head.column}}
{{#body.row as |row|}} Flow Group
{{#row.cell}} {{/head.column}}
{{#link-to entityRoute entity.id}}
<span class="entity-list__title">
{{entity.name}}
</span>
{{/link-to}}
{{dataset-owner-list owners=entity.owners datasetName=entity.name}} {{#head.column}}
Flow Name
{{/head.column}}
{{#if entity.formatedModified}} {{#head.column}}
<span>Last Modified:</span> Flow Level
{{/head.column}}
<span title="{{entity.formatedModified}}"> {{#head.column}}
{{moment-from-now entity.formatedModified }} Job Count
</span> {{/head.column}}
{{/if}}
{{/row.cell}} {{#head.column}}
{{#row.cell}} Creation Time
{{datasets/dataset-actions actionItems=actionItems}} {{/head.column}}
{{/row.cell}}
{{/body.row}} {{#head.column}}
{{/each}} Modified Time
{{/table.body}} {{/head.column}}
{{/dataset-table}} {{/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}}
<span class="entity-list__title">
{{entity.name}}
</span>
{{/link-to}}
<br>
{{#if (eq currentEntity 'metrics')}}
{{entity.group}} - {{entity.dashboardName}}
<br>
{{entity.description}}
{{/if}}
{{#if (eq currentEntity 'datasests')}}
{{dataset-owner-list owners=entity.owners datasetName=entity.name}}
{{/if}}
{{#if entity.formatedModified}}
<span>Last Modified:</span>
<span title="{{entity.formatedModified}}">
{{moment-from-now entity.formatedModified }}
</span>
{{/if}}
{{/row.cell}}
{{#row.cell}}
{{datasets/dataset-actions actionItems=actionItems}}
{{/row.cell}}
{{/body.row}}
{{/each}}
{{/table.body}}
{{/dataset-table}}
{{/if}}

View File

@ -1,238 +1,228 @@
<div id="metric" class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row">
<div class="col-xs-6"> <div class="col-xs-6">
<h3>{{ model.name }}</h3> <h3>{{ model.name }}</h3>
</div> </div>
<div class="col-xs-6 text-right"> <div class="col-xs-6 text-right">
<ul class="datasetDetailsLinks"> <ul class="datasetDetailsLinks">
<li> <li>
<i class="fa fa-share-alt"></i> <i class="fa fa-share-alt"></i>
<span class="hidden-sm hidden-xs"> <span class="hidden-sm hidden-xs">Share</span>
Share
</span>
</li> </li>
<li> <li>
{{#metric-watch metric=model showText=true getMetrics='getMetrics'}} {{metric-watch metric=model showText=true}}
{{/metric-watch}}
</li> </li>
{{#if showLineage}} {{#if showLineage}}
<li> <li>
<a target="_blank" href={{lineageUrl}}> <a target="_blank" href={{lineageUrl}}>
<i class="fa fa-sitemap"></i> <i class="fa fa-sitemap" aria-label="View Lineage"></i>
<span class="hidden-sm hidden-xs"> <span class="hidden-sm hidden-xs">View Lineage</span>
View Lineage
</span>
</a> </a>
</li> </li>
{{/if}} {{/if}}
</ul> </ul>
</div> </div>
<div class="col-xs-12"> <div class="col-xs-12">
Metric Description: Metric Description:
<a <a
href="#" href="#"
data-name="description" data-name="description"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter description" data-title="Enter description"
data-emptytext="Please Input" data-emptytext="Please Input"
data-placeholder="Please Input" data-placeholder="Please Input">
>
{{model.description}} {{model.description}}
</a> </a>
</div> </div>
</div> </div>
<table class="tree table table-bordered">
<table class="nacho-table nacho-table--bordered nacho-table--dynamic">
<tbody> <tbody>
<tr class="result"> <tr class="result">
<td class="span2" style="min-width:200px;">Dashboard Name</td> <td class="span2" style="min-width:200px;">Dashboard Name</td>
<td> <td>
<a <a
href="#" href="#"
data-name="dashboardName" data-name="dashboardName"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter dashboard name" data-title="Enter dashboard name"
data-defaultValue="Please Input" data-defaultValue="Please Input"
data-emptytext="Please Input" data-emptytext="Please Input"
data-value="{{model.dashboardName}}" data-value="{{model.dashboardName}}">
>
{{model.dashboardName}} {{model.dashboardName}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Category</td> <td>Metric Category</td>
<td> <td>
<a <a
href="#" href="#"
data-name="category" data-name="category"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter metric category" data-title="Enter metric category"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.category}} {{model.category}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Group</td> <td>Metric Group</td>
<td> <td>
<a <a
href="#" href="#"
data-name="group" data-name="group"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter group" data-title="Enter group"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.group}} {{model.group}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Type</td> <td>Metric Type</td>
<td> <td>
<a <a
href="#" href="#"
data-name="refIDType" data-name="refIDType"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter Type" data-title="Enter Type"
data-placement="right" data-emptytext="Please Input"
data-emptytext="Please Input" data-value={{model.refIDType}}>
data-value={{model.refIDType}}
>
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Grain</td> <td>Metric Grain</td>
<td> <td>
<a <a
href="#" href="#"
data-name="grain" data-name="grain"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter grain" data-title="Enter grain"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.grain}} {{model.grain}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Formula</td> <td>Metric Formula</td>
<td> <td>
{{ace-editor content=model.formula itemId=model.id savePath="/api/v1/metrics/{id}/update" saveParam="formula"}} {{ace-editor content=model.formula itemId=model.id savePath="/api/v1/metrics/{id}/update" saveParam="formula"}}
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Display Factor</td> <td>Metric Display Factor</td>
<td> <td>
<a <a
href="#" href="#"
data-name="displayFactory" data-name="displayFactory"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter display factor" data-title="Enter display factor"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.displayFactory}} {{model.displayFactory}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Display Factor Sym</td> <td>Metric Display Factor Sym</td>
<td> <td>
<a <a
href="#" href="#"
data-name="displayFactorSym" data-name="displayFactorSym"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter display factor symbol" data-title="Enter display factor symbol"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.displayFactorSym}} {{model.displayFactorSym}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Sub Category</td> <td>Metric Sub Category</td>
<td> <td>
<a <a
href="#" href="#"
data-name="subCategory" data-name="subCategory"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter sub category" data-title="Enter sub category"
data-placement="right" data-emptytext="Please Input">
data-emptytext="Please Input"
>
{{model.subCategory}} {{model.subCategory}}
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Source</td> <td>Metric Source</td>
<td> <td>
<a <a
href="#" href="#"
data-name="source" data-name="source"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter source" data-title="Enter source"
data-placement="right" data-emptytext="Please Input"
data-emptytext="Please Input" data-value={{model.source}}>
data-value={{model.source}}
>
</a> </a>
</td> </td>
</tr> </tr>
<tr class="result"> <tr class="result">
<td>Metric Source Type</td> <td>Metric Source Type</td>
<td> <td>
<a <a
href="#" href="#"
data-name="sourceType" data-name="sourceType"
data-pk="{{model.id}}" data-pk="{{model.id}}"
class="xeditable" class="xeditable"
data-type="text" data-type="text"
data-placement="right" data-placement="right"
data-title="Enter source type" data-title="Enter source type"
data-placement="right" data-emptytext="Please Input"
data-emptytext="Please Input" data-value={{model.sourceType}}>
data-value={{model.sourceType}}
>
</a> </a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -69,6 +69,7 @@
</div> </div>
</div> </div>
{{dataset-owner-list owners=owners datasetName=model.name}} {{dataset-owner-list owners=owners datasetName=model.name}}
{{#if hasinstances}} {{#if hasinstances}}
<div class="row"> <div class="row">
<span class="col-xs-1">Instances:</span> <span class="col-xs-1">Instances:</span>
@ -91,6 +92,7 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if hasversions}} {{#if hasversions}}
<div class="row"> <div class="row">
<span class="col-xs-1">Versions:</span> <span class="col-xs-1">Versions:</span>
@ -114,6 +116,7 @@
</div> </div>
{{/if}} {{/if}}
</div> </div>
<ul class="nav nav-tabs nav-justified tabbed-navigation-list"> <ul class="nav nav-tabs nav-justified tabbed-navigation-list">
{{#unless isPinot}} {{#unless isPinot}}
<li id="properties"> <li id="properties">
@ -168,6 +171,7 @@
</a> </a>
</li> </li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
{{#unless isPinot}} {{#unless isPinot}}
<div id="propertiestab" class="tab-pane"> <div id="propertiestab" class="tab-pane">

View File

@ -85,11 +85,7 @@
</div> </div>
</div> </div>
{{else}} {{else}}
<div id="pagedJobs"> <div class="row">
<div class="row"> {{outlet}}
<div class="col-xs-12">
{{outlet}}
</div>
</div>
</div> </div>
{{/if}} {{/if}}

View File

@ -1 +1,51 @@
<div class="container">
{{!--TODO: Make into Component--}}
<ul class="nacho-breadcrumbs">
{{#each breadcrumbs as |crumb|}}
<li class="nacho-breadcrumbs__crumb">
{{link-to crumb.crumb "browse.entity" "flows" (query-params page=1 urn=crumb.urn name=crumb.name)
class="nacho-breadcrumbs__crumb__grain"}}
</li>
{{/each}}
</ul>
{{#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}}
</div>
{{outlet}} {{outlet}}

View File

@ -1 +1,13 @@
{{outlet}} <div class="container">
{{!--TODO: Make into Component--}}
<ul class="nacho-breadcrumbs">
{{#each breadcrumbs as |crumb|}}
<li class="nacho-breadcrumbs__crumb">
{{link-to crumb.crumb "browse.entity" "metrics" (query-params page=1 name=crumb.name)
class="nacho-breadcrumbs__crumb__grain"}}
</li>
{{/each}}
</ul>
{{metric-detail showLineage=showLineage model=model}}
</div>

View File

@ -5,10 +5,16 @@
* @type {RegExp} * @type {RegExp}
*/ */
const urnRegex = /([a-z_]+):\/{3}([a-z0-9_\-\/\{\}]*)/i; 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 * Asserts that a provided string matches the urn pattern above
* @param {String} candidateUrn the string to test on * @param {String} candidateUrn the string to test on
*/ */
export default candidateUrn => urnRegex.test(String(candidateUrn)); export default candidateUrn => urnRegex.test(String(candidateUrn));
export { urnRegex }; export { urnRegex, specialFlowUrnRegex };

View File

@ -96,6 +96,9 @@ module.exports = function(defaults) {
app.import( app.import(
'bower_components/jsondiffpatch/public/formatters-styles/annotated.css' '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/main.css');
app.import('vendor/legacy_styles/comments.css'); app.import('vendor/legacy_styles/comments.css');
app.import('vendor/legacy_styles/wherehows.css'); app.import('vendor/legacy_styles/wherehows.css');
@ -129,9 +132,9 @@ module.exports = function(defaults) {
app.import('vendor/CsvToMarkdown.js'); app.import('vendor/CsvToMarkdown.js');
app.import('vendor/typeahead.jquery.js'); app.import('vendor/typeahead.jquery.js');
app.import('bower_components/marked/marked.min.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-noconflict/ace.js');
app.import('bower_components/ace-builds/src-min/theme-github.js'); app.import('bower_components/ace-builds/src-noconflict/theme-github.js');
app.import('bower_components/ace-builds/src-min/mode-sql.js'); app.import('bower_components/ace-builds/src-noconflict/mode-sql.js');
app.import('bower_components/toastr/toastr.min.js'); app.import('bower_components/toastr/toastr.min.js');
app.import('bower_components/highcharts/highcharts.js'); app.import('bower_components/highcharts/highcharts.js');
app.import( app.import(
@ -140,6 +143,9 @@ module.exports = function(defaults) {
app.import( app.import(
'bower_components/jsondiffpatch/public/build/jsondiffpatch-formatters.min.js' '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])); return app.toTree(new MergeTrees([faFontTree, bsFontTree, treegridImgTree]));
}; };

View File

@ -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);
});