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 {
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

View File

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

View File

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

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

View File

@ -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';

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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;
/**

View File

@ -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(
'<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>' +
'<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' +
'Hopefully, this saves you some scrolling!</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>. ' +
'Hopefully, this saves you some scrolling!</p>'
);
/**
@ -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.<Object>} 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')());

View File

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

View File

@ -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.')
})

View File

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

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 {
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:

View File

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

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';
/**
* 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
});

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';
/**
* 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:

View File

@ -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({

View File

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

View File

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

View File

@ -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<V, void>|Promise<V, X>}
*/
model({ metric_id }) {
const metricsUrl = `${metricsUrlRoot}/${metric_id}`;
return fetch(metricsUrl).then(response => response.json());
}
});

View File

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

View File

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

View File

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

View File

@ -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"}}
<div class="browse-nav__item">

View File

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

View File

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

View File

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

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}
*/
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 };

View File

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

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