mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-27 00:40:06 +00:00
adds browse feature for flows and metrics
This commit is contained in:
parent
5dede95d9d
commit
a9c622c832
@ -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
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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}`)));
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
/**
|
||||
|
||||
@ -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')());
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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.')
|
||||
})
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
6
wherehows-web/app/controllers/flows/flow.js
Normal file
6
wherehows-web/app/controllers/flows/flow.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Ember from 'ember';
|
||||
const { Controller } = Ember;
|
||||
|
||||
export default Controller.extend({
|
||||
queryParams: ['name']
|
||||
});
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
*/
|
||||
.ember-radio-button {
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +78,8 @@ $pad-width: 16px;
|
||||
.ember-radio-button {
|
||||
position: relative;
|
||||
left: 26px;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
cursor: pointer;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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]));
|
||||
};
|
||||
|
||||
12
wherehows-web/tests/unit/controllers/flows/flow-test.js
Normal file
12
wherehows-web/tests/unit/controllers/flows/flow-test.js
Normal 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);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user