v2: add intergration tests for container components. temporarily skips acceptance tests for datasets. further i implementation for v2 api integration and urn handling

This commit is contained in:
Seyi Adebajo 2018-02-21 09:46:04 -08:00
parent 1dbcaf1476
commit 0eb87ec99b
48 changed files with 1179 additions and 607 deletions

View File

@ -71,10 +71,10 @@ export default class DatasetAuthors extends Component {
/** /**
* A list of valid owner type strings returned from the remote api endpoint * A list of valid owner type strings returned from the remote api endpoint
* @type {Array<string>} * @type {Array<OwnerType>}
* @memberof DatasetAuthors * @memberof DatasetAuthors
*/ */
ownerTypes: Array<string>; ownerTypes: Array<OwnerType>;
/** /**
* Flag that resolves in the affirmative if the number of confirmed owner is less the minimum required * Flag that resolves in the affirmative if the number of confirmed owner is less the minimum required

View File

@ -204,7 +204,13 @@ export default class DatasetCompliance extends ObservableDecorator {
isCompliancePolicyAvailable: boolean = false; isCompliancePolicyAvailable: boolean = false;
showAllDatasetMemberData: boolean; showAllDatasetMemberData: boolean;
complianceInfo: void | IComplianceInfo; complianceInfo: void | IComplianceInfo;
complianceSuggestion: IComplianceSuggestion;
/**
* Suggested values for compliance types e.g. identifier type and/or logical type
* @type {IComplianceSuggestion | void}
*/
complianceSuggestion: IComplianceSuggestion | void;
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>; schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>;
onReset: <T extends { status: ApiStatus }>() => Promise<T>; onReset: <T extends { status: ApiStatus }>() => Promise<T>;
onSave: <T extends { status: ApiStatus }>() => Promise<T>; onSave: <T extends { status: ApiStatus }>() => Promise<T>;
@ -676,10 +682,12 @@ export default class DatasetCompliance extends ObservableDecorator {
function(this: DatasetCompliance): ISchemaFieldsToPolicy { function(this: DatasetCompliance): ISchemaFieldsToPolicy {
const { complianceEntities = [], modifiedTime = '0' } = get(this, 'complianceInfo') || {}; const { complianceEntities = [], modifiedTime = '0' } = get(this, 'complianceInfo') || {};
// Truncated list of Dataset field names and data types currently returned from the column endpoint // Truncated list of Dataset field names and data types currently returned from the column endpoint
const columnFieldProps = get(this, 'schemaFieldNamesMappedToDataTypes').map(({ fieldName, dataType }) => ({ const columnFieldProps = getWithDefault(this, 'schemaFieldNamesMappedToDataTypes', []).map(
identifierField: fieldName, ({ fieldName, dataType }) => ({
dataType identifierField: fieldName,
})); dataType
})
);
return this.mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldProps, complianceEntities, { return this.mapColumnIdFieldsToCurrentPrivacyPolicy(columnFieldProps, complianceEntities, {
policyModificationTime: modifiedTime policyModificationTime: modifiedTime

View File

@ -1,6 +1,8 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { set, get, setProperties, getProperties } from '@ember/object'; import { set, get, setProperties, getProperties } from '@ember/object';
import { warn } from '@ember/debug'; import { warn } from '@ember/debug';
import { action } from 'ember-decorators/object';
import { IDatasetColumn, IDatasetColumnWithHtmlComments } from 'wherehows-web/typings/api/datasets/columns';
/** /**
* Cached module reference to the class name for visually hiding toggled off schema view * Cached module reference to the class name for visually hiding toggled off schema view
@ -23,16 +25,22 @@ export default class DatasetSchema extends Component {
json = '{}'; json = '{}';
/** /**
* Reference to the jsonViewer dom element * List of schema properties for the dataset
* @type {Element} * @type {IDatasetColumnWithHtmlComments | IDatasetColumn}
*/ */
jsonViewer = <Element | null>null; schemas: Array<IDatasetColumnWithHtmlComments | IDatasetColumn>;
/**
* Reference to the jsonViewer dom element
* @type {Element | null}
*/
jsonViewer: Element | null = null;
/** /**
* Reference to the jsonTable dom element * Reference to the jsonTable dom element
* @type {Element} * @type {Element | null}
*/ */
jsonTable = <Element | null>null; jsonTable: Element | null = null;
/** /**
* Constructs a readable JSON structure of the dataset schema * Constructs a readable JSON structure of the dataset schema
@ -54,6 +62,19 @@ export default class DatasetSchema extends Component {
*/ */
buildTableView = (jsonTable: Element) => $(jsonTable).treegrid(); buildTableView = (jsonTable: Element) => $(jsonTable).treegrid();
/**
* Builds or rebuild the dataset schema / json view based on a flag to toggle this behaviour
*/
buildView() {
if (get(this, 'isTable')) {
const jsonTable = get(this, 'jsonTable');
jsonTable && this.buildTableView(jsonTable);
} else {
const jsonViewer = get(this, 'jsonViewer');
jsonViewer && this.buildJsonView(jsonViewer);
}
}
/** /**
* Retains references to the DOM elements for showing the schema * Retains references to the DOM elements for showing the schema
*/ */
@ -71,13 +92,7 @@ export default class DatasetSchema extends Component {
} }
didReceiveAttrs(this: DatasetSchema) { didReceiveAttrs(this: DatasetSchema) {
if (get(this, 'isTable')) { this.buildView();
const jsonTable = get(this, 'jsonTable');
jsonTable && this.buildTableView(jsonTable);
} else {
const jsonViewer = get(this, 'jsonViewer');
jsonViewer && this.buildJsonView(jsonViewer);
}
} }
didRender() { didRender() {
@ -91,21 +106,21 @@ export default class DatasetSchema extends Component {
}); });
} }
actions = { @action
/** /**
* Handles the toggle for which schema view to be rendered. * Handles the toggling of table vs. json view of dataset schema information
* Currently toggled between a table render and a JSON view * @param {"table" | "json"} view
* @param {"table" | "json"} [view = table] */
*/ showView(this: DatasetSchema, view: 'table' | 'json' = 'table') {
showView(this: DatasetSchema, view: 'table' | 'json' = 'table') { const isTable = set(this, 'isTable', view === 'table');
const isTable = set(this, 'isTable', view === 'table'); const { jsonViewer, jsonTable } = getProperties(this, ['jsonTable', 'jsonViewer']);
const { jsonViewer, jsonTable } = getProperties(this, ['jsonTable', 'jsonViewer']);
if (jsonTable && jsonViewer) { if (jsonTable && jsonViewer) {
isTable this.buildView();
? (jsonViewer.classList.add(hiddenClassName), jsonTable.classList.remove(hiddenClassName))
: (jsonViewer.classList.remove(hiddenClassName), jsonTable.classList.add(hiddenClassName)); isTable
} ? (jsonViewer.classList.add(hiddenClassName), jsonTable.classList.remove(hiddenClassName))
: (jsonViewer.classList.remove(hiddenClassName), jsonTable.classList.add(hiddenClassName));
} }
}; }
} }

View File

@ -0,0 +1,166 @@
import Component from '@ember/component';
import { get, set, setProperties, getProperties } from '@ember/object';
import { task, TaskInstance } from 'ember-concurrency';
import { action } from 'ember-decorators/object';
import { IDatasetColumn } from 'wherehows-web/typings/api/datasets/columns';
import { IComplianceInfo, IComplianceSuggestion } from 'wherehows-web/typings/api/datasets/compliance';
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
import { IDatasetSchema } from 'wherehows-web/typings/api/datasets/schema';
import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes';
import {
IReadComplianceResult,
readDatasetComplianceByUrn,
readDatasetComplianceSuggestionByUrn,
saveDatasetComplianceByUrn
} from 'wherehows-web/utils/api';
import { columnDataTypesAndFieldNames } from 'wherehows-web/utils/api/datasets/columns';
import { readDatasetSchemaByUrn } from 'wherehows-web/utils/api/datasets/schema';
import { readComplianceDataTypes } from 'wherehows-web/utils/api/list/compliance-datatypes';
export default class DatasetComplianceContainer extends Component {
/**
* Mapping of field names on the dataset schema to the respective dataType
* @type {Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>>}
*/
schemaFieldNamesMappedToDataTypes: Array<Pick<IDatasetColumn, 'dataType' | 'fieldName'>> = [];
/**
* List of compliance data-type objects
* @type {Array<IComplianceDataType>}
*/
complianceDataTypes: Array<IComplianceDataType> = [];
/**
* Flag indicating that the compliance information for this dataset is new
* @type {boolean}
*/
isNewComplianceInfo: boolean = false;
/**
* Object containing the suggested values for the compliance form
* @type {IComplianceSuggestion | void}
*/
complianceSuggestion: IComplianceSuggestion | void;
/**
* Object containing the compliance information for the dataset
* @type {IComplianceInfo | void}
*/
complianceInfo: IComplianceInfo | void;
/**
* The platform / db that the dataset is persisted
* @type {IDatasetView.platform}
*/
platform: IDatasetView['platform'];
/**
* Flag indicating if the dataset has a schema representation
* @type {boolean}
*/
schemaless: boolean = false;
/**
* The nativeName of the dataset
* @type {string}
*/
datasetName: string = '';
/**
* The urn identifier for the dataset
* @type {string}
*/
urn: string;
didInsertElement() {
get(this, 'getContainerDataTask').perform();
}
didUpdateAttrs() {
get(this, 'getContainerDataTask').perform();
}
/**
* An async parent task to group all data tasks for this container component
* @type {Task<TaskInstance<Promise<any>>, (a?: any) => TaskInstance<TaskInstance<Promise<any>>>>}
*/
getContainerDataTask = task(function*(
this: DatasetComplianceContainer
): IterableIterator<TaskInstance<Promise<any>>> {
const tasks = Object.values(
getProperties(this, [
'getComplianceTask',
'getComplianceDataTypesTask',
'getComplianceSuggestionsTask',
'getDatasetSchemaTask'
])
);
yield* tasks.map(task => task.perform());
});
/**
* Reads the compliance properties for the dataset
* @type {Task<Promise<IReadComplianceResult>, (a?: any) => TaskInstance<Promise<IReadComplianceResult>>>}
*/
getComplianceTask = task(function*(
this: DatasetComplianceContainer
): IterableIterator<Promise<IReadComplianceResult>> {
const { isNewComplianceInfo, complianceInfo } = yield readDatasetComplianceByUrn(get(this, 'urn'));
setProperties(this, { isNewComplianceInfo, complianceInfo });
});
/**
* Reads the compliance data types
* @type {Task<Promise<Array<IComplianceDataType>>, (a?: any) => TaskInstance<Promise<Array<IComplianceDataType>>>>}
*/
getComplianceDataTypesTask = task(function*(
this: DatasetComplianceContainer
): IterableIterator<Promise<Array<IComplianceDataType>>> {
const complianceDataTypes = yield readComplianceDataTypes();
set(this, 'complianceDataTypes', complianceDataTypes);
});
/**
* Reads the suggestions for the compliance properties on the dataset
* @type {Task<Promise<IComplianceSuggestion>, (a?: any) => TaskInstance<Promise<IComplianceSuggestion>>>}
*/
getComplianceSuggestionsTask = task(function*(
this: DatasetComplianceContainer
): IterableIterator<Promise<IComplianceSuggestion>> {
const complianceSuggestion = yield readDatasetComplianceSuggestionByUrn(get(this, 'urn'));
set(this, 'complianceSuggestion', complianceSuggestion);
});
/**
* Reads the schema properties for the dataset
* @type {Task<Promise<IDatasetSchema>, (a?: any) => TaskInstance<Promise<IDatasetSchema>>>}
*/
getDatasetSchemaTask = task(function*(this: DatasetComplianceContainer): IterableIterator<Promise<IDatasetSchema>> {
const { columns, schemaless } = yield readDatasetSchemaByUrn(get(this, 'urn'));
const schemaFieldNamesMappedToDataTypes = columnDataTypesAndFieldNames(columns);
setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless });
});
@action
/**
* Persists the updates to the compliance policy on the remote host
* @param {IComplianceInfo} complianceInfo
* @return {Promise<void>}
*/
savePrivacyCompliancePolicy(complianceInfo: IComplianceInfo): Promise<void> {
return saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo);
}
@action
/**
* Resets the compliance information for the dataset with the previously persisted properties
*/
resetPrivacyCompliancePolicy() {
get(this, 'getComplianceTask').perform();
}
}

View File

@ -0,0 +1,80 @@
import Component from '@ember/component';
import { get, set, getProperties } from '@ember/object';
import { task, TaskInstance } from 'ember-concurrency';
import { action } from 'ember-decorators/object';
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import {
OwnerType,
readDatasetOwnersByUrn,
readDatasetOwnerTypesWithoutConsumer,
updateDatasetOwnersByUrn
} from 'wherehows-web/utils/api/datasets/owners';
export default class DatasetOwnershipContainer extends Component {
/**
* The urn identifier for the dataset
* @type {string}
*/
urn: string;
/**
* List of owners for the dataset
* @type {Array<IOwner>}
*/
owners: Array<IOwner>;
/**
* List of types available for a dataset owner
* @type {Array<OwnerType>}
*/
ownerTypes: Array<OwnerType>;
didInsertElement() {
get(this, 'getContainerDataTask').perform();
}
didUpdateAttrs() {
get(this, 'getContainerDataTask').perform();
}
/**
* An async parent task to group all data tasks for this container component
* @type {Task<TaskInstance<Promise<any>>, (a?: any) => TaskInstance<TaskInstance<Promise<any>>>>}
*/
getContainerDataTask = task(function*(this: DatasetOwnershipContainer): IterableIterator<TaskInstance<Promise<any>>> {
const tasks = Object.values(getProperties(this, ['getDatasetOwnersTask', 'getDatasetOwnerTypesTask']));
yield* tasks.map(task => task.perform());
});
/**
* Reads the owners for this dataset
* @type {Task<Promise<Array<IOwner>>, (a?: any) => TaskInstance<Promise<Array<IOwner>>>>}
*/
getDatasetOwnersTask = task(function*(this: DatasetOwnershipContainer): IterableIterator<Promise<Array<IOwner>>> {
const owners = yield readDatasetOwnersByUrn(get(this, 'urn'));
set(this, 'owners', owners);
});
/**
* Reads the owner types available
* @type {Task<Promise<Array<OwnerType>>, (a?: any) => TaskInstance<Promise<Array<OwnerType>>>>}
*/
getDatasetOwnerTypesTask = task(function*(
this: DatasetOwnershipContainer
): IterableIterator<Promise<Array<OwnerType>>> {
const ownerTypes = yield readDatasetOwnerTypesWithoutConsumer();
set(this, 'ownerTypes', ownerTypes);
});
@action
/**
* Persists the changes to the owners list
* @param {Array<IOwner>} updatedOwners
* @return {Promise<void>}
*/
saveOwnerChanges(updatedOwners: Array<IOwner>): Promise<void> {
return updateDatasetOwnersByUrn(get(this, 'urn'), '', updatedOwners);
}
}

View File

@ -0,0 +1,69 @@
import Component from '@ember/component';
import { get, setProperties } from '@ember/object';
import { task } from 'ember-concurrency';
import { action } from 'ember-decorators/object';
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
import { readDatasetByUrn } from 'wherehows-web/utils/api/datasets/dataset';
import { updateDatasetDeprecationByUrn } from 'wherehows-web/utils/api/datasets/properties';
export default class DatasetPropertiesContainer extends Component {
/**
* The urn identifier for the dataset
* @type {string}
*/
urn: string;
/**
* Flag indicating that the dataset is deprecated
* @type {boolean | null}
*/
deprecated: boolean | null;
/**
* Text string, intended to indicate the reason for deprecation
* @type {string | null}
*/
deprecationNote: string | null;
/**
* THe list of properties for the dataset, currently unavailable for v2
* @type {any[]}
*/
properties: Array<never> = [];
constructor() {
super(...arguments);
this.deprecationNote || (this.deprecationNote = '');
}
didInsertElement() {
get(this, 'getDeprecationPropertiesTask').perform();
}
didUpdateAttrs() {
get(this, 'getDeprecationPropertiesTask').perform();
}
/**
* Reads the persisted deprecation properties for the dataset
* @type {Task<Promise<IDatasetView>, (a?: any) => TaskInstance<Promise<IDatasetView>>>}
*/
getDeprecationPropertiesTask = task(function*(
this: DatasetPropertiesContainer
): IterableIterator<Promise<IDatasetView>> {
const { deprecated, deprecationNote } = yield readDatasetByUrn(get(this, 'urn'));
setProperties(this, { deprecated, deprecationNote });
});
@action
/**
* Persists the changes to the dataset deprecation properties upstream
* @param {boolean} isDeprecated
* @param {string} updatedDeprecationNote
* @return {Promise<void>}
*/
async updateDeprecation(isDeprecated: boolean, updatedDeprecationNote: string): Promise<void> {
await updateDatasetDeprecationByUrn(get(this, 'urn'), isDeprecated, updatedDeprecationNote);
get(this, 'getDeprecationPropertiesTask').perform();
}
}

View File

@ -0,0 +1,48 @@
import Component from '@ember/component';
import { get, setProperties } from '@ember/object';
import { task } from 'ember-concurrency';
import { IDatasetColumn, IDatasetColumnWithHtmlComments } from 'wherehows-web/typings/api/datasets/columns';
import { IDatasetSchema } from 'wherehows-web/typings/api/datasets/schema';
import { augmentObjectsWithHtmlComments } from 'wherehows-web/utils/api/datasets/columns';
import { readDatasetSchemaByUrn } from 'wherehows-web/utils/api/datasets/schema';
export default class DatasetSchemaContainer extends Component {
/**
* The urn identifier for the dataset
* @type {string}
*/
urn: string;
/**
* json string for the dataset schema properties
* @type {string}
*/
json: string;
/**
* List of schema properties for the dataset
* @type {IDatasetColumnWithHtmlComments | IDatasetColumn}
*/
schemas: Array<IDatasetColumnWithHtmlComments | IDatasetColumn>;
didInsertElement() {
get(this, 'getDatasetSchemaTask').perform();
}
didUpdateAttrs() {
get(this, 'getDatasetSchemaTask').perform();
}
/**
* Reads the schema for the dataset
* @type {Task<Promise<IDatasetSchema>, (a?: any) => TaskInstance<Promise<IDatasetSchema>>>}
*/
getDatasetSchemaTask = task(function*(this: DatasetSchemaContainer): IterableIterator<Promise<IDatasetSchema>> {
let schemas,
{ columns, rawSchema: json } = yield readDatasetSchemaByUrn(get(this, 'urn'));
schemas = augmentObjectsWithHtmlComments(columns);
json || (json = '{}');
setProperties(this, { schemas, json });
});
}

View File

@ -4,9 +4,6 @@ import $ from 'jquery';
export default Controller.extend({ export default Controller.extend({
urn: null, urn: null,
currentName: null,
urnWatched: false,
detailview: true,
previousPage: computed('model.data.page', function() { previousPage: computed('model.data.page', function() {
var model = this.get('model'); var model = this.get('model');
if (model && model.data && model.data.page) { if (model && model.data && model.data.page) {

View File

@ -11,6 +11,7 @@ import {
deleteDatasetComment, deleteDatasetComment,
updateDatasetComment updateDatasetComment
} from 'wherehows-web/utils/api'; } from 'wherehows-web/utils/api';
import { encodeUrn } from 'wherehows-web/utils/validators/urn';
import { updateDatasetDeprecation } from 'wherehows-web/utils/api/datasets/properties'; import { updateDatasetDeprecation } from 'wherehows-web/utils/api/datasets/properties';
import { readDatasetView } from 'wherehows-web/utils/api/datasets/dataset'; import { readDatasetView } from 'wherehows-web/utils/api/datasets/dataset';
import { readDatasetOwners, updateDatasetOwners } from 'wherehows-web/utils/api/datasets/owners'; import { readDatasetOwners, updateDatasetOwners } from 'wherehows-web/utils/api/datasets/owners';
@ -28,14 +29,8 @@ export default class extends Controller.extend({
*/ */
notifications: service(), notifications: service(),
isTable: true,
hasImpacts: false,
hasSamples: false,
currentVersion: '0',
latestVersion: '0',
init() { init() {
setProperties(this, { setProperties(this, {
ownerTypes: [],
userTypes: [ userTypes: [
{ name: 'Corporate User', value: 'urn:li:corpuser' }, { name: 'Corporate User', value: 'urn:li:corpuser' },
{ name: 'Group User', value: 'urn:li:corpGroup' } { name: 'Group User', value: 'urn:li:corpGroup' }
@ -205,19 +200,6 @@ export default class extends Controller.extend({
}, },
actions: { actions: {
/**
* Updates the dataset's deprecation properties
* @param {boolean} isDeprecated
* @param {string} deprecationNote
* @return {IDatasetView}
*/
async updateDeprecation(isDeprecated, deprecationNote) {
const datasetId = get(this, 'datasetId');
await updateDatasetDeprecation(datasetId, isDeprecated, deprecationNote);
return set(this, 'datasetView', await readDatasetView(datasetId));
},
/** /**
* Action handler creates a dataset comment with the type and text pas * Action handler creates a dataset comment with the type and text pas
* @param {CommentTypeUnion} type the comment type * @param {CommentTypeUnion} type the comment type
@ -324,12 +306,29 @@ export default class extends Controller.extend({
} }
} }
}) { }) {
/**
* Enum of tab properties
* @type {Tabs}
*/
tabIds = Tabs; tabIds = Tabs;
/**
* The currently selected tab in view
* @type {Tabs}
*/
tabSelected; tabSelected;
/**
* Converts the uri on a model to a usable URN format
* @type {ComputedProperty<string>}
*/
encodedUrn = computed('model', function() {
const { uri = '' } = get(this, 'model');
return encodeUrn(uri);
});
constructor() { constructor() {
super(); super(...arguments);
this.tabSelected || (this.tabSelected = Tabs.Ownership); this.tabSelected || (this.tabSelected = Tabs.Ownership);
} }
@ -342,6 +341,6 @@ export default class extends Controller.extend({
// if the tab selection is same as current, noop // if the tab selection is same as current, noop
return get(this, 'tabSelected') === tabSelected return get(this, 'tabSelected') === tabSelected
? void 0 ? void 0
: this.transitionToRoute(`datasets.dataset.${tabSelected}`, get(this, 'datasetId')); : this.transitionToRoute(`datasets.dataset.${tabSelected}`, get(this, 'encodedUrn'));
} }
} }

View File

@ -1,7 +1,6 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { set, get, setProperties } from '@ember/object'; import { set, get, setProperties } from '@ember/object';
import { inject } from '@ember/service'; import { inject } from '@ember/service';
import $ from 'jquery';
import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities'; import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities';
import { readDatasetCompliance, readDatasetComplianceSuggestion } from 'wherehows-web/utils/api/datasets/compliance'; import { readDatasetCompliance, readDatasetComplianceSuggestion } from 'wherehows-web/utils/api/datasets/compliance';
import { readNonPinotProperties, readPinotProperties } from 'wherehows-web/utils/api/datasets/properties'; import { readNonPinotProperties, readPinotProperties } from 'wherehows-web/utils/api/datasets/properties';
@ -15,29 +14,12 @@ import {
import { readDatasetOwners, getUserEntities } from 'wherehows-web/utils/api/datasets/owners'; import { readDatasetOwners, getUserEntities } from 'wherehows-web/utils/api/datasets/owners';
import { isRequiredMinOwnersNotConfirmed } from 'wherehows-web/constants/datasets/owner'; import { isRequiredMinOwnersNotConfirmed } from 'wherehows-web/constants/datasets/owner';
import { import { readDatasetById, readDatasetByUrn } from 'wherehows-web/utils/api/datasets/dataset';
readDatasetById, import isUrn, { isWhUrn, isLiUrn, convertWhUrnToLiUrn, encodeUrn, decodeUrn } from 'wherehows-web/utils/validators/urn';
datasetUrnToId,
readDatasetView,
readDatasetByUrn
} from 'wherehows-web/utils/api/datasets/dataset';
import { isWhUrn, isLiUrn } from 'wherehows-web/utils/validators/urn';
import { checkAclAccess } from 'wherehows-web/utils/api/datasets/acl-access'; import { checkAclAccess } from 'wherehows-web/utils/api/datasets/acl-access';
import { currentUser } from 'wherehows-web/utils/api/authentication'; import { currentUser } from 'wherehows-web/utils/api/authentication';
import { refreshModelQueryParams } from 'wherehows-web/utils/helpers/routes';
const { getJSON } = $;
// TODO: DSS-6581 Move to URL retrieval module
const datasetsUrlRoot = '/api/v1/datasets';
const datasetUrl = id => `${datasetsUrlRoot}/${id}`;
const ownerTypeUrlRoot = '/api/v1/owner/types';
const getDatasetSampleUrl = id => `${datasetUrl(id)}/sample`;
const getDatasetImpactAnalysisUrl = id => `${datasetUrl(id)}/impacts`;
const getDatasetDependsUrl = id => `${datasetUrl(id)}/depends`;
const getDatasetPartitionsUrl = id => `${datasetUrl(id)}/access`;
const getDatasetReferencesUrl = id => `${datasetUrl(id)}/references`;
const getDatasetInstanceUrl = id => `${datasetUrl(id)}/instances`;
const getDatasetVersionUrl = (id, dbId) => `${datasetUrl(id)}/versions/db/${dbId}`;
export default Route.extend({ export default Route.extend({
/** /**
@ -45,31 +27,39 @@ export default Route.extend({
* @type {Ember.Service} * @type {Ember.Service}
*/ */
configurator: inject(), configurator: inject(),
/**
* Reference to the application notifications Service
* @type {ComputedProperty<Notifications>}
*/
notifications: inject(),
queryParams: { queryParams: refreshModelQueryParams(['urn']),
urn: {
refreshModel: true
}
},
/** /**
* Reads the dataset given a identifier from the dataset endpoint * Reads the dataset given a identifier from the dataset endpoint
* @param {string} dataset_id a identifier / id for the dataset to be fetched * @param {string} dataset_id a identifier / id for the dataset to be fetched
* @param {string} [urn] optional urn identifier for dataset * @param {string} [urn] optional urn identifier for dataset
* @return {Promise<IDataset>} * @return {Promise<IDataset|IDatasetView>}
*/ */
async model({ dataset_id: datasetId, urn }) { async model({ dataset_id: identifier, urn }) {
if (datasetId === 'urn') { const isIdentifierUrn = isUrn(decodeUrn(String(identifier)));
if (isWhUrn(urn)) {
return readDatasetById(await datasetUrnToId(urn)); if (identifier === 'urn' || isIdentifierUrn) {
isIdentifierUrn && (urn = identifier);
const decodedUrn = decodeUrn(urn);
isWhUrn(decodedUrn) && (urn = convertWhUrnToLiUrn(decodedUrn));
if (isLiUrn(decodeUrn(urn))) {
return await readDatasetByUrn(encodeUrn(urn));
} }
if (isLiUrn(urn)) { get(this, 'notifications.notify')('error', {
return await readDatasetByUrn(urn); content: 'Could not adequately determine the URN for the requested dataset.'
} });
} }
return await readDatasetById(datasetId); return await readDatasetById(identifier);
}, },
/** /**
@ -80,259 +70,22 @@ export default Route.extend({
set(controller, 'urn', void 0); set(controller, 'urn', void 0);
}, },
//TODO: DSS-6632 Correct server-side if status:error and record not found but response is 200OK async setupController(controller, model) {
setupController(controller, model) { set(controller, 'model', model);
let source = ''; setProperties(controller, {
let id = 0; isInternal: await get(this, 'configurator').getConfig('isInternal')
let urn = ''; // ...properties,
// requiredMinNotConfirmed: isRequiredMinOwnersNotConfirmed(owners)
if (model && model.id) { });
({ id, source, urn } = model);
let { originalSchema = null } = model;
this.controllerFor('datasets').set('detailview', true);
if (originalSchema) {
set(model, 'schema', originalSchema);
}
controller.set('model', model);
} else if (model.dataset) {
({ id, source, urn } = model.dataset);
controller.set('model', model.dataset);
this.controllerFor('datasets').set('detailview', true);
}
// Don't set default zero Ids on controller
if (id) {
id = +id;
controller.set('datasetId', id);
/**
* ****************************
* Note: Refactor in progress *
* ****************************
* async IIFE sets the the complianceInfo and schemaFieldNamesMappedToDataTypes
* at once so observers will be buffered
* @param {number} id the dataset id
* @return {Promise.<void>}
*/
(async id => {
try {
let properties;
const [
{ schemaless, columns },
compliance,
complianceDataTypes,
complianceSuggestion,
datasetComments,
isInternal,
datasetView,
owners,
{ userEntitiesSource, userEntitiesMaps }
] = await Promise.all([
readDatasetColumns(id),
readDatasetCompliance(id),
readComplianceDataTypes(),
readDatasetComplianceSuggestion(id),
readDatasetComments(id),
get(this, 'configurator').getConfig('isInternal'),
readDatasetView(id),
readDatasetOwners(id),
getUserEntities()
]);
const { complianceInfo, isNewComplianceInfo } = compliance;
const schemas = augmentObjectsWithHtmlComments(columns);
if (String(source).toLowerCase() === 'pinot') {
properties = await readPinotProperties(id);
} else {
properties = { properties: await readNonPinotProperties(id) };
}
setProperties(controller, {
complianceInfo,
complianceDataTypes,
isNewComplianceInfo,
complianceSuggestion,
datasetComments,
schemaless,
schemas,
isInternal,
datasetView,
schemaFieldNamesMappedToDataTypes: columnDataTypesAndFieldNames(columns),
...properties,
owners,
userEntitiesMaps,
userEntitiesSource,
requiredMinNotConfirmed: isRequiredMinOwnersNotConfirmed(owners)
});
} catch (e) {
throw e;
}
})(id);
}
Promise.resolve(getJSON(getDatasetInstanceUrl(id)))
.then(({ status, instances = [] }) => {
if (status === 'ok' && instances.length) {
const [firstInstance = {}] = instances;
const { dbId } = firstInstance;
setProperties(controller, {
instances,
hasinstances: true,
currentInstance: firstInstance,
latestInstance: firstInstance
});
return Promise.resolve(getJSON(getDatasetVersionUrl(id, dbId))).then(({ status, versions = [] }) => {
if (status === 'ok' && versions.length) {
const [firstVersion] = versions;
setProperties(controller, {
versions,
hasversions: true,
currentVersion: firstVersion,
latestVersion: firstVersion
});
}
return Promise.reject(new Error('Dataset versions request failed.'));
});
}
return Promise.reject(new Error('Dataset instances request failed.'));
})
.catch(() => {
setProperties(controller, {
hasinstances: false,
hasversions: false,
currentInstance: '0',
latestInstance: '0',
currentVersion: '0',
latestVersion: '0'
});
});
// If urn exists, create a breadcrumb list // If urn exists, create a breadcrumb list
// TODO: DSS-7068 Refactoring in progress , move this to a computed prop on a container component // TODO: DSS-7068 Refactoring in progress , move this to a computed prop on a container component
// FIXME: DSS-7068 browse.entity?urn route does not exist for last item in breadcrumb i.e. the dataset // FIXME: DSS-7068 browse.entity?urn route does not exist for last item in breadcrumb i.e. the dataset
// currently being viewed. Should this even be a link in the first place? // currently being viewed. Should this even be a link in the first place?
if (urn) { if (model.uri) {
set(controller, 'breadcrumbs', makeUrnBreadcrumbs(urn)); set(controller, 'breadcrumbs', makeUrnBreadcrumbs(model.uri));
} }
// Get the list of ownerTypes from endpoint,
// then prevent display of the `consumer`
// insert on controller
Promise.resolve(getJSON(ownerTypeUrlRoot)).then(({ status, ownerTypes = [] }) => {
ownerTypes = ownerTypes
.filter(ownerType => String(ownerType).toLowerCase() !== 'consumer')
.sort((a, b) => a.localeCompare(b));
status === 'ok' && set(controller, 'ownerTypes', ownerTypes);
});
if (source.toLowerCase() !== 'pinot') {
Promise.resolve(getJSON(getDatasetSampleUrl(id)))
.then(({ status, sampleData = {} }) => {
if (status === 'ok') {
const { sample = [] } = sampleData;
const { length } = sample;
if (length) {
const samplesObject = sample.reduce(
(sampleObject, record, index) => ((sampleObject[`record${index}`] = record), sampleObject),
{}
);
// TODO: DSS-6122 Refactor global function reference
const node = window.JsonHuman.format(samplesObject);
controller.set('hasSamples', true);
// TODO: DSS-6122 Refactor setTimeout
setTimeout(function() {
const jsonHuman = document.getElementById('datasetSampleData-json-human');
if (jsonHuman) {
if (jsonHuman.children && jsonHuman.children.length) {
jsonHuman.removeChild(jsonHuman.childNodes[jsonHuman.children.length - 1]);
}
jsonHuman.appendChild(node);
}
}, 500);
}
return;
}
return Promise.reject(new Error('Dataset sample request failed.'));
})
.catch(() => set(controller, 'hasSamples', false));
}
Promise.resolve(getJSON(getDatasetImpactAnalysisUrl(id)))
.then(({ status, impacts = [] }) => {
if (status === 'ok') {
if (impacts.length) {
return setProperties(controller, {
hasImpacts: true,
impacts: impacts
});
}
}
return Promise.reject(new Error('Dataset impact analysis request failed.'));
})
.catch(() => set(controller, 'hasImpacts', false));
Promise.resolve(getJSON(getDatasetDependsUrl(id)))
.then(({ status, depends = [] }) => {
if (status === 'ok') {
if (depends.length) {
// TODO: DSS-6122 Refactor setTimeout,
// global function reference
setTimeout(window.initializeDependsTreeGrid, 500);
return setProperties(controller, {
depends,
hasDepends: true
});
}
}
return Promise.reject(new Error('Dataset depends request failed.'));
})
.catch(() => set(controller, 'hasDepends', false));
Promise.resolve(getJSON(getDatasetPartitionsUrl(id)))
.then(({ status, access = [] }) => {
if (status === 'ok' && access.length) {
return setProperties(controller, {
hasAccess: true,
accessibilities: access
});
}
return Promise.reject(new Error('Dataset partitions request failed.'));
})
.catch(() => set(controller, 'hasAccess', false));
Promise.resolve(getJSON(getDatasetReferencesUrl(id)))
.then(({ status, references = [] }) => {
if (status === 'ok' && references.length) {
setTimeout(window.initializeReferencesTreeGrid, 500);
return setProperties(controller, {
references,
hasReferences: true
});
}
return Promise.reject(new Error('Dataset references request failed.'));
})
.catch(() => set(controller, 'hasReferences', false));
// TODO: Get current user ACL permission info for ACL access tab // TODO: Get current user ACL permission info for ACL access tab
Promise.resolve(currentUser()) Promise.resolve(currentUser())
.then(userInfo => { .then(userInfo => {
@ -353,13 +106,5 @@ export default Route.extend({
currentUserInfo: '' currentUserInfo: ''
}); });
}); });
},
actions: {
getDataset() {
Promise.resolve(getJSON(datasetUrl(this.get('controller.model.id')))).then(
({ status, dataset }) => status === 'ok' && set(this, 'controller.model', dataset)
);
}
} }
}); });

View File

@ -11,4 +11,15 @@
&__actions { &__actions {
margin-top: item-spacing(2); margin-top: item-spacing(2);
} }
&__toggle-header {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: item-spacing(3);
&__label {
margin: item-spacing(0 2 0 0);
}
}
} }

View File

@ -7,6 +7,7 @@
{{#if isOwnerInActive}} {{#if isOwnerInActive}}
<br>
<span class="nacho-button nacho-button--small dataset-author-record__indicator--inactive"> <span class="nacho-button nacho-button--small dataset-author-record__indicator--inactive">
Inactive Inactive
</span> </span>

View File

@ -1,15 +1,23 @@
<form {{action "onSave" on="submit"}}> <form {{action "onSave" on="submit"}}>
<label for="dataset-is-deprecated">
Dataset is deprecated?
</label>
{{input <div class="dataset-deprecation-toggle__toggle-header">
id="dataset-is-deprecated"
type="checkbox" <p class="dataset-deprecation-toggle__toggle-header__label">
title="Is this dataset deprecated?" Is this dataset deprecated?
checked=(readonly deprecatedAlias) </p>
change=(action "toggleDeprecatedStatus")
}} {{input
id="dataset-is-deprecated"
type="checkbox"
title="Is this dataset deprecated?"
class="toggle-switch toggle-switch--light"
checked=(readonly deprecatedAlias)
change=(action "toggleDeprecatedStatus")
}}
<label for="dataset-is-deprecated" class="toggle-button">
</label>
</div>
{{#if deprecatedAlias}} {{#if deprecatedAlias}}

View File

@ -0,0 +1,12 @@
{{dataset-compliance
datasetName=datasetName
schemaless=schemaless
platform=platform
complianceInfo=complianceInfo
complianceSuggestion=complianceSuggestion
isNewComplianceInfo=isNewComplianceInfo
schemaFieldNamesMappedToDataTypes=schemaFieldNamesMappedToDataTypes
complianceDataTypes=complianceDataTypes
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")
}}

View File

@ -0,0 +1,5 @@
{{dataset-authors
owners=owners
ownerTypes=ownerTypes
save=(action "saveOwnerChanges")
}}

View File

@ -0,0 +1,7 @@
{{dataset-deprecation
deprecated=deprecated
deprecationNote=deprecationNote
onUpdateDeprecation=(action "updateDeprecation")
}}
{{dataset-property properties=properties}}

View File

@ -0,0 +1,4 @@
{{dataset-schema
json=json
schemas=schemas
}}

View File

@ -11,7 +11,7 @@
<div class="row"> <div class="row">
<div class="col-xs-5"> <div class="col-xs-5">
{{#if datasetView.removed}} {{#if model.removed}}
<span class="removed-dataset"> <span class="removed-dataset">
REMOVED REMOVED
</span> </span>
@ -28,8 +28,8 @@
</sup> </sup>
{{/if}} {{/if}}
{{#if datasetView.deprecated}} {{#if model.deprecated}}
{{#link-to "datasets.dataset.properties" datasetId}} {{#link-to "datasets.dataset.properties" encodedUrn}}
<span class="deprecated-dataset"> <span class="deprecated-dataset">
DEPRECATED DEPRECATED
@ -48,110 +48,10 @@
{{/link-to}} {{/link-to}}
{{/if}} {{/if}}
<h3>{{ model.name }}</h3> <h3>{{ model.nativeName }}</h3>
</div>
<div class="col-xs-7 text-right">
<ul class="datasetDetailsLinks">
<li>
{{#dataset-favorite dataset=model action="didFavorite"}}
{{/dataset-favorite}}
<span class="hidden-sm hidden-xs">
{{#if model.isFavorite}}
Unfavorite
{{else}}
Favorite
{{/if}}
</span>
</li>
{{#if model.hdfsBrowserLink}}
<li>
<a target="_blank" href={{model.hdfsBrowserLink}}
title="View on HDFS">
<i class="fa fa-database"></i>
<span class="hidden-sm hidden-xs">
HDFS
</span>
</a>
</li>
{{/if}}
<li>
{{#link-to 'lineage.dataset' model.id title='View Lineage'}}
<i class="fa fa-sitemap"></i>
<span class="hidden-sm hidden-xs">Lineage</span>
{{/link-to}}
</li>
{{#if model.hasSchemaHistory}}
<li>
<a target="_blank" href={{schemaHistoryUrl}}
title="View Schema History">
<i class="fa fa-history"></i>
<span class="hidden-sm hidden-xs">
Schema History
</span>
</a>
</li>
{{/if}}
<li>
{{#dataset-watch dataset=model getDatasets="getDataset"}}
{{/dataset-watch}}
<span class="hidden-xs hidden-sm">
{{#if model.isWatched}}
Unwatch
{{else}}
Watch
{{/if}}
</span>
</li>
</ul>
</div> </div>
</div> </div>
{{dataset-owner-list owners=owners datasetName=model.name}} {{dataset-owner-list owners=owners datasetName=model.nativeName}}
{{#if hasinstances}}
<div class="row">
<span class="col-xs-1">Instances:</span>
<div class="btn-toolbar col-xs-11" role="toolbar">
{{#each instances as |instance index|}}
<div class="btn-group" role="group">
{{#if index}}
<button type="button" data-value="{{ instance.dbCode }}" class="btn btn-default instance-btn" {{action
"updateInstance" instance}}>
{{ instance.dbCode }}
</button>
{{else}}
<button type="button" data-value="{{ instance.dbCode }}" class="btn btn-primary instance-btn" {{action
"updateInstance" instance}}>
{{ instance.dbCode }}
</button>
{{/if}}
</div>
{{/each}}
</div>
</div>
{{/if}}
{{#if hasversions}}
<div class="row">
<span class="col-xs-1">Versions:</span>
<div class="btn-toolbar col-xs-11" role="toolbar">
{{#each versions as |version index|}}
<div class="btn-group" role="group">
{{#if index}}
<button type="button" data-value="{{ version }}" class="btn btn-default version-btn" {{action
"updateVersion" version}}>
{{ version }}
</button>
{{else}}
<button type="button" data-value="{{ version }}" class="btn btn-primary version-btn" {{action
"updateVersion" version}}>
{{ version }}
</button>
{{/if}}
</div>
{{/each}}
</div>
</div>
{{/if}}
</div> </div>
{{#ivy-tabs selection=tabSelected as |tabs|}} {{#ivy-tabs selection=tabSelected as |tabs|}}
@ -169,8 +69,6 @@
{{#tablist.tab tabIds.Access on-select=(action "tabSelectionChanged")}}ACL Access{{/tablist.tab}} {{#tablist.tab tabIds.Access on-select=(action "tabSelectionChanged")}}ACL Access{{/tablist.tab}}
</span> </span>
{{#tablist.tab tabIds.Comments on-select=(action "tabSelectionChanged")}}Comments{{/tablist.tab}}
{{#tablist.tab tabIds.Schema on-select=(action "tabSelectionChanged")}}Schema{{/tablist.tab}} {{#tablist.tab tabIds.Schema on-select=(action "tabSelectionChanged")}}Schema{{/tablist.tab}}
{{#tablist.tab tabIds.Ownership on-select=(action "tabSelectionChanged")}} {{#tablist.tab tabIds.Ownership on-select=(action "tabSelectionChanged")}}
@ -191,26 +89,14 @@
{{/tablist.tab}} {{/tablist.tab}}
{{/if}} {{/if}}
{{#unless isSFDC}}
{{#tablist.tab tabIds.SampleData on-select=(action "tabSelectionChanged")}}
Sample Data{{/tablist.tab}}
{{/unless}}
{{#tablist.tab tabIds.Relations on-select=(action "tabSelectionChanged")}}
Relations{{/tablist.tab}}
{{/tabs.tablist}} {{/tabs.tablist}}
{{#tabs.tabpanel tabIds.Properties}} {{#tabs.tabpanel tabIds.Properties}}
{{#unless isPinot}} {{datasets/containers/dataset-properties
{{dataset-deprecation urn=encodedUrn
deprecated=datasetView.deprecated deprecated=model.deprecated
deprecationNote=datasetView.deprecationNote deprecationNote=model.deprecationNote
onUpdateDeprecation=(action "updateDeprecation") }}
}}
{{dataset-property properties=properties}}
{{/unless}}
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Comments}} {{#tabs.tabpanel tabIds.Comments}}
@ -223,45 +109,18 @@
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Schema}} {{#tabs.tabpanel tabIds.Schema}}
{{dataset-schema {{datasets/containers/dataset-schema urn=encodedUrn}}
isTable=isTable
json=model.schema
schemas=schemas
}}
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Ownership}} {{#tabs.tabpanel tabIds.Ownership}}
{{dataset-authors {{datasets/containers/dataset-ownership urn=encodedUrn}}
owners=owners
ownerTypes=ownerTypes
save=(action "saveOwnerChanges")
}}
{{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.SampleData}}
{{#unless isSFDC}}
{{dataset-sample hasSamples=hasSamples isPinot=isPinot columns=columns samples=samples}}
{{/unless}}
{{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Relations}}
{{#unless isSFDC}}
{{dataset-relations hasDepends=hasDepends depends=depends hasReferences=hasReferences references=references}}
{{/unless}}
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Compliance}} {{#tabs.tabpanel tabIds.Compliance}}
{{dataset-compliance {{datasets/containers/dataset-compliance
datasetName=model.name urn=encodedUrn
schemaless=schemaless platform=model.platform
platform=datasetView.platform datasetName=model.nativeName
complianceInfo=complianceInfo
complianceSuggestion=complianceSuggestion
isNewComplianceInfo=isNewComplianceInfo
schemaFieldNamesMappedToDataTypes=schemaFieldNamesMappedToDataTypes
complianceDataTypes=complianceDataTypes
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")
}} }}
{{/tabs.tabpanel}} {{/tabs.tabpanel}}

View File

@ -37,4 +37,14 @@ interface IOwnerPostResponse {
msg?: string; msg?: string;
} }
export { IOwnerPostResponse, IOwnerResponse, IOwner }; /**
* Describes the properties on a response to a request for owner types
* @interface
*/
interface IOwnerTypeResponse {
status: ApiStatus;
ownerTypes?: Array<OwnerType>;
msg?: string;
}
export { IOwnerPostResponse, IOwnerResponse, IOwner, IOwnerTypeResponse };

View File

@ -0,0 +1,22 @@
import { IDatasetColumn } from 'wherehows-web/typings/api/datasets/columns';
/**
* Describes the properties on a dataset schema object
* @interface
*/
interface IDatasetSchema {
schemaless: boolean;
rawSchema: null | string;
keySchema: null | string;
columns: Array<IDatasetColumn>;
}
/**
* Describes the properties on a response to a request for dataset schema
* @interface
*/
interface IDatasetSchemaGetResponse {
schema: IDatasetSchema;
}
export { IDatasetSchema, IDatasetSchemaGetResponse };

View File

@ -27,14 +27,18 @@ const datasetColumnUrlById = (id: number): string => `${datasetUrlById(id)}/colu
* Maps an object with a column prop to an object containing markdown comments, if the dataset has a comment attribute * Maps an object with a column prop to an object containing markdown comments, if the dataset has a comment attribute
* @template T * @template T
* @param {T} objectWithComment * @param {T} objectWithComment
* @return {T & {commentHtml: string} | {} & T} * @returns {(T | T & {commentHtml: string})}
*/ */
const augmentWithHtmlComment = <T extends { comment: string }>(objectWithComment: T) => { const augmentWithHtmlComment = <T extends { comment: string }>(
objectWithComment: T
): T | T & { commentHtml: string } => {
const { comment } = objectWithComment; const { comment } = objectWithComment;
// TODO: DSS-6122 Refactor global function reference to marked // TODO: DSS-6122 Refactor global function reference to marked
// not using spread operator here: https://github.com/Microsoft/TypeScript/issues/10727 // not using spread operator here: https://github.com/Microsoft/TypeScript/issues/10727
// current ts version: 2.5.3 // current ts version: 2.5.3
return Object.assign({}, objectWithComment, comment && { commentHtml: window.marked(comment).htmlSafe() }); return comment
? Object.assign({}, objectWithComment, { commentHtml: window.marked(comment).htmlSafe() })
: objectWithComment;
}; };
/** /**
@ -55,7 +59,9 @@ const columnDataTypeAndFieldName = ({
* Takes a list of objects with comments and returns an array of objects with comments or html comments * Takes a list of objects with comments and returns an array of objects with comments or html comments
* @type {(array: Array<T extends { comment: string } & Object>) => Array<T | T extends { commentHtml: string }>} * @type {(array: Array<T extends { comment: string } & Object>) => Array<T | T extends { commentHtml: string }>}
*/ */
const augmentObjectsWithHtmlComments = arrayMap(augmentWithHtmlComment); const augmentObjectsWithHtmlComments = arrayMap<IDatasetColumn, IDatasetColumnWithHtmlComments | IDatasetColumn>(
augmentWithHtmlComment
);
/** /**
* Takes a list of IDatasetColumn / IDatasetColumn with html comments and pulls the dataType and and fullFieldPath (as fieldName) attributes * Takes a list of IDatasetColumn / IDatasetColumn with html comments and pulls the dataType and and fullFieldPath (as fieldName) attributes

View File

@ -1,9 +1,10 @@
import Ember from 'ember'; import Ember from 'ember';
import { IDatasetComment, IDatasetCommentsGetResponse } from 'wherehows-web/typings/api/datasets/comments'; import { IDatasetComment, IDatasetCommentsGetResponse } from 'wherehows-web/typings/api/datasets/comments';
import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { datasetUrlById, datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared';
import { ApiStatus } from 'wherehows-web/utils/api/shared'; import { ApiStatus } from 'wherehows-web/utils/api/shared';
import { getJSON } from 'wherehows-web/utils/api/fetcher';
const { $: { getJSON, post, ajax } } = Ember; const { $: { getJSON: $getJSON, post, ajax } } = Ember;
// TODO: DSS-6122 Create and move to Error module // TODO: DSS-6122 Create and move to Error module
/** /**
@ -25,6 +26,13 @@ const csrfToken = '_UNUSED_';
*/ */
const datasetCommentsUrlById = (id: number): string => `${datasetUrlById(id)}/comments`; const datasetCommentsUrlById = (id: number): string => `${datasetUrlById(id)}/comments`;
/**
* Returns the url for a dataset comment by urn
* @param {string} urn
* @return {string}
*/
const datasetCommentsUrnByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/comments`;
/** /**
* Gets a specific comment on a dataset * Gets a specific comment on a dataset
* @param {number} datasetId the id of the dataset * @param {number} datasetId the id of the dataset
@ -40,7 +48,7 @@ const datasetCommentUrlById = (datasetId: number, commentId: number): string =>
* @return {Promise<Array<IDatasetComment>>} * @return {Promise<Array<IDatasetComment>>}
*/ */
const readDatasetComments = async (id: number): Promise<Array<IDatasetComment>> => { const readDatasetComments = async (id: number): Promise<Array<IDatasetComment>> => {
const response: IDatasetCommentsGetResponse = await Promise.resolve(getJSON(datasetCommentsUrlById(id))); const response: IDatasetCommentsGetResponse = await Promise.resolve($getJSON(datasetCommentsUrlById(id)));
const { status, data: { comments } } = response; const { status, data: { comments } } = response;
if (status === ApiStatus.OK) { if (status === ApiStatus.OK) {
@ -50,6 +58,18 @@ const readDatasetComments = async (id: number): Promise<Array<IDatasetComment>>
throw new Error(datasetCommentsApiException); throw new Error(datasetCommentsApiException);
}; };
/**
* Reads the dataset comments related to the urn
* @param {string} urn
* @return {Promise<Array<IDatasetComment>>}
*/
const readDatasetCommentsByUrn = async (urn: string): Promise<Array<IDatasetComment>> => {
const { data: { comments } } = await getJSON<Pick<IDatasetCommentsGetResponse, 'data'>>({
url: datasetCommentsUrnByUrn(urn)
});
return comments;
};
/** /**
* Posts a new comment on a dataset * Posts a new comment on a dataset
* @param {number} id the id of the dataset * @param {number} id the id of the dataset
@ -138,4 +158,10 @@ const updateDatasetComment = async (
} }
}; };
export { readDatasetComments, createDatasetComment, deleteDatasetComment, updateDatasetComment }; export {
readDatasetComments,
createDatasetComment,
deleteDatasetComment,
updateDatasetComment,
readDatasetCommentsByUrn
};

View File

@ -1,6 +1,6 @@
import { assert } from '@ember/debug'; import { assert } from '@ember/debug';
import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/compliance-policy'; import { createInitialComplianceInfo } from 'wherehows-web/utils/datasets/compliance-policy';
import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { datasetUrlById, datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared';
import { ApiStatus } from 'wherehows-web/utils/api/shared'; import { ApiStatus } from 'wherehows-web/utils/api/shared';
import { import {
IComplianceGetResponse, IComplianceGetResponse,
@ -8,7 +8,7 @@ import {
IComplianceSuggestion, IComplianceSuggestion,
IComplianceSuggestionResponse IComplianceSuggestionResponse
} from 'wherehows-web/typings/api/datasets/compliance'; } from 'wherehows-web/typings/api/datasets/compliance';
import { getJSON } from 'wherehows-web/utils/api/fetcher'; import { getJSON, postJSON } from 'wherehows-web/utils/api/fetcher';
/** /**
* Constructs the dataset compliance url * Constructs the dataset compliance url
@ -17,6 +17,13 @@ import { getJSON } from 'wherehows-web/utils/api/fetcher';
*/ */
const datasetComplianceUrlById = (id: number): string => `${datasetUrlById(id)}/compliance`; const datasetComplianceUrlById = (id: number): string => `${datasetUrlById(id)}/compliance`;
/**
* Returns the url for a datasets compliance policy by urn
* @param {string} urn
* @return {string}
*/
const datasetComplianceUrlByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/compliance`;
/** /**
* Constructs the compliance suggestions url based of the compliance id * Constructs the compliance suggestions url based of the compliance id
* @param {number} id the id of the dataset * @param {number} id the id of the dataset
@ -24,6 +31,13 @@ const datasetComplianceUrlById = (id: number): string => `${datasetUrlById(id)}/
*/ */
const datasetComplianceSuggestionsUrlById = (id: number): string => `${datasetComplianceUrlById(id)}/suggestions`; const datasetComplianceSuggestionsUrlById = (id: number): string => `${datasetComplianceUrlById(id)}/suggestions`;
/**
* Returns the url for a dataset compliance suggestion by urn
* @param {string} urn
* @return {string}
*/
const datasetComplianceSuggestionUrlByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/complianceSuggestion`;
/** /**
* Determines if the client app should 'new' a compliance policy * Determines if the client app should 'new' a compliance policy
* If the endpoint responds with a failed status, and the msg contains the indicator that a compliance does not exist * If the endpoint responds with a failed status, and the msg contains the indicator that a compliance does not exist
@ -35,14 +49,21 @@ const requiresCompliancePolicyCreation = ({ status, msg }: IComplianceGetRespons
return status === ApiStatus.FAILED && String(msg).includes(notFound); return status === ApiStatus.FAILED && String(msg).includes(notFound);
}; };
/**
* Describes the properties on a map generated by reading the compliance policy for a dataset
* @interface
*/
export interface IReadComplianceResult {
isNewComplianceInfo: boolean;
complianceInfo: IComplianceInfo;
}
/** /**
* Fetches the current compliance policy for a dataset with thi given id * Fetches the current compliance policy for a dataset with thi given id
* @param {number} id the id of the dataset * @param {number} id the id of the dataset
* @returns {(Promise<{ isNewComplianceInfo: boolean; complianceInfo: IComplianceInfo }>)} * @returns {(Promise<IReadComplianceResult>)}
*/ */
const readDatasetCompliance = async ( const readDatasetCompliance = async (id: number): Promise<IReadComplianceResult> => {
id: number
): Promise<{ isNewComplianceInfo: boolean; complianceInfo: IComplianceInfo }> => {
assert(`Expected id to be a number but received ${typeof id}`, typeof id === 'number'); assert(`Expected id to be a number but received ${typeof id}`, typeof id === 'number');
const response = await getJSON<IComplianceGetResponse>({ url: datasetComplianceUrlById(id) }); const response = await getJSON<IComplianceGetResponse>({ url: datasetComplianceUrlById(id) });
@ -60,6 +81,35 @@ const readDatasetCompliance = async (
throw new Error(msg); throw new Error(msg);
}; };
/**
* Reads the dataset compliance policy by urn
* @param {string} urn
* @return {Promise<IReadComplianceResult>}
*/
const readDatasetComplianceByUrn = async (urn: string): Promise<IReadComplianceResult> => {
let { complianceInfo } = await getJSON<Pick<IComplianceGetResponse, 'complianceInfo'>>({
url: datasetComplianceUrlByUrn(urn)
});
const isNewComplianceInfo = !complianceInfo;
if (isNewComplianceInfo) {
complianceInfo = createInitialComplianceInfo(urn);
}
return { isNewComplianceInfo, complianceInfo: complianceInfo! };
};
/**
* Persists the dataset compliance policy
* @param {string} urn
* @param {IComplianceInfo} complianceInfo
* @return {Promise<void>}
*/
const saveDatasetComplianceByUrn = (urn: string, complianceInfo: IComplianceInfo): Promise<void> => {
const url = datasetUrlByUrn(urn);
return postJSON<void>({ url, data: complianceInfo });
};
/** /**
* Requests the compliance suggestions for a given dataset Id and returns the suggestion list * Requests the compliance suggestions for a given dataset Id and returns the suggestion list
* @param {number} id the id of the dataset * @param {number} id the id of the dataset
@ -73,4 +123,29 @@ const readDatasetComplianceSuggestion = async (id: number): Promise<IComplianceS
return complianceSuggestion; return complianceSuggestion;
}; };
export { readDatasetCompliance, readDatasetComplianceSuggestion, datasetComplianceUrlById }; /**
* Reads the suggestions for a dataset compliance policy by urn
* @param {string} urn
* @return {Promise<IComplianceSuggestion>}
*/
const readDatasetComplianceSuggestionByUrn = async (urn: string): Promise<IComplianceSuggestion> => {
let complianceSuggestion: IComplianceSuggestion = <IComplianceSuggestion>{};
try {
({ complianceSuggestion = <IComplianceSuggestion>{} } = await getJSON<
Pick<IComplianceSuggestionResponse, 'complianceSuggestion'>
>({ url: datasetComplianceSuggestionUrlByUrn(urn) }));
} catch {
return complianceSuggestion;
}
return complianceSuggestion;
};
export {
readDatasetCompliance,
readDatasetComplianceSuggestion,
datasetComplianceUrlById,
readDatasetComplianceByUrn,
saveDatasetComplianceByUrn,
readDatasetComplianceSuggestionByUrn
};

View File

@ -1,13 +1,19 @@
import { IOwner, IOwnerPostResponse, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners'; import {
IOwner,
IOwnerPostResponse,
IOwnerResponse,
IOwnerTypeResponse
} from 'wherehows-web/typings/api/datasets/owners';
import { import {
IPartyEntity, IPartyEntity,
IPartyEntityResponse, IPartyEntityResponse,
IPartyProps, IPartyProps,
IUserEntityMap IUserEntityMap
} from 'wherehows-web/typings/api/datasets/party-entities'; } from 'wherehows-web/typings/api/datasets/party-entities';
import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { datasetUrlById, datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared';
import { getJSON, postJSON } from 'wherehows-web/utils/api/fetcher'; import { getJSON, postJSON } from 'wherehows-web/utils/api/fetcher';
import { getApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared'; import { getApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared';
import { arrayFilter, arrayMap } from 'wherehows-web/utils/array';
/** /**
* Defines a string enum for valid owner types * Defines a string enum for valid owner types
@ -57,8 +63,25 @@ enum OwnerSource {
*/ */
const datasetOwnersUrlById = (id: number): string => `${datasetUrlById(id)}/owners`; const datasetOwnersUrlById = (id: number): string => `${datasetUrlById(id)}/owners`;
/**
* Returns the dataset owners url by urn
* @param {string} urn
* @return {string}
*/
const datasetOwnersUrlByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/owners`;
/**
* Returns the party entities url
* @type {string}
*/
const partyEntitiesUrl = `${getApiRoot()}/party/entities`; const partyEntitiesUrl = `${getApiRoot()}/party/entities`;
/**
* Returns the owner types url
* @return {string}
*/
const datasetOwnerTypesUrl = () => `${getApiRoot()}/owner/types`;
/** /**
* Requests the list of dataset owners from the GET endpoint, converts the modifiedTime property * Requests the list of dataset owners from the GET endpoint, converts the modifiedTime property
* to a date object * to a date object
@ -68,15 +91,38 @@ const partyEntitiesUrl = `${getApiRoot()}/party/entities`;
const readDatasetOwners = async (id: number): Promise<Array<IOwner>> => { const readDatasetOwners = async (id: number): Promise<Array<IOwner>> => {
const { owners = [], status, msg } = await getJSON<IOwnerResponse>({ url: datasetOwnersUrlById(id) }); const { owners = [], status, msg } = await getJSON<IOwnerResponse>({ url: datasetOwnersUrlById(id) });
if (status === ApiStatus.OK) { if (status === ApiStatus.OK) {
return owners.map(owner => ({ return ownersWithModifiedTimeAsDate(owners);
...owner,
modifiedTime: new Date(<number>owner.modifiedTime!) // Api response is always in number format
}));
} }
throw new Error(msg); throw new Error(msg);
}; };
/**
* Modifies an owner object by applying the modified date property as a Date object
* @param {IOwner} owner
* @return {IOwner}
*/
const ownerWithModifiedTimeAsDate = (owner: IOwner): IOwner => ({
...owner,
modifiedTime: new Date(<number>owner.modifiedTime)
}); // Api response is always in number format
/**
* Modifies a list of owners with a modified date property as a Date object
* @type {(array: Array<IOwner>) => Array<IOwner>}
*/
const ownersWithModifiedTimeAsDate = arrayMap(ownerWithModifiedTimeAsDate);
/**
* Reads the owners for dataset by urn
* @param {string} urn
* @return {Promise<Array<IOwner>>}
*/
const readDatasetOwnersByUrn = async (urn: string): Promise<Array<IOwner>> => {
const { owners = [] } = await getJSON<Pick<IOwnerResponse, 'owners'>>({ url: datasetOwnersUrlByUrn(urn) });
return ownersWithModifiedTimeAsDate(owners);
};
/** /**
* Persists the updated list of dataset owners * Persists the updated list of dataset owners
* @param {number} id the id of the dataset * @param {number} id the id of the dataset
@ -105,6 +151,47 @@ const updateDatasetOwners = async (
throw new Error(msg); throw new Error(msg);
}; };
/**
* Updates the owners on a dataset by urn
* @param {string} urn
* @param {string} csrfToken
* @param {Array<IOwner>} updatedOwners
* @return {Promise<void>}
*/
const updateDatasetOwnersByUrn = (urn: string, csrfToken: string = '', updatedOwners: Array<IOwner>): Promise<void> => {
return postJSON<void>({
url: datasetOwnersUrlByUrn(urn),
headers: { 'csrf-token': csrfToken },
data: {
csrfToken,
owners: updatedOwners
}
});
};
/**
* Reads the owner types list on a dataset
* @return {Promise<Array<OwnerType>>}
*/
const readDatasetOwnerTypes = async (): Promise<Array<OwnerType>> => {
const url = datasetOwnerTypesUrl();
const { ownerTypes = [] } = await getJSON<IOwnerTypeResponse>({ url });
return ownerTypes.sort((a: string, b: string) => a.localeCompare(b));
};
/**
* Determines if an owner type supplied is not of type consumer
* @param {OwnerType} ownerType
* @return {boolean}
*/
const isNotAConsumer = (ownerType: OwnerType): boolean => ownerType !== OwnerType.Consumer;
/**
* Reads the dataset owner types and filters out the OwnerType.Consumer type from the list
* @return {Promise<Array<OwnerType>>}
*/
const readDatasetOwnerTypesWithoutConsumer = async () => arrayFilter(isNotAConsumer)(await readDatasetOwnerTypes());
/** /**
* Requests party entities and if the response status is OK, resolves with an array of entities * Requests party entities and if the response status is OK, resolves with an array of entities
* @return {Promise<Array<IPartyEntity>>} * @return {Promise<Array<IPartyEntity>>}
@ -179,6 +266,9 @@ const readPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): IUserEntityMa
export { export {
readDatasetOwners, readDatasetOwners,
readDatasetOwnersByUrn,
updateDatasetOwnersByUrn,
readDatasetOwnerTypesWithoutConsumer,
readPartyEntities, readPartyEntities,
readPartyEntitiesMap, readPartyEntitiesMap,
getUserEntities, getUserEntities,

View File

@ -1,7 +1,7 @@
import { warn } from '@ember/debug'; import { warn } from '@ember/debug';
import { ApiStatus } from 'wherehows-web/utils/api'; import { ApiStatus } from 'wherehows-web/utils/api';
import { getJSON, putJSON } from 'wherehows-web/utils/api/fetcher'; import { getJSON, putJSON } from 'wherehows-web/utils/api/fetcher';
import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; import { datasetUrlById, datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared';
import { import {
IDatasetProperties, IDatasetProperties,
IDatasetPropertiesGetResponse, IDatasetPropertiesGetResponse,
@ -27,6 +27,13 @@ const datasetPropertiesUrlById = (id: number) => `${datasetUrlById(id)}/properti
const datasetDeprecationUrlById = (id: number) => `${datasetUrlById(id)}/deprecate`; const datasetDeprecationUrlById = (id: number) => `${datasetUrlById(id)}/deprecate`;
/**
* Returns the url for a dataset deprecation endpoint by urn
* @param {string} urn
* @return {string}
*/
const datasetDeprecationUrlByUrn = (urn: string) => `${datasetUrlByUrn(urn)}/deprecate`;
/** /**
* Reads the response from the dataset properties endpoint and returns properties if found * Reads the response from the dataset properties endpoint and returns properties if found
* @param {number} id the dataset id to get properties for * @param {number} id the dataset id to get properties for
@ -197,4 +204,31 @@ const updateDatasetDeprecation = async (id: number, deprecated: boolean, depreca
} }
}; };
export { readDatasetProperties, readNonPinotProperties, readPinotProperties, updateDatasetDeprecation }; /**
* Persists the changes to a datasets deprecation properties by urn
* @param {string} urn
* @param {boolean} deprecated
* @param {string} deprecationNote
* @return {Promise<void>}
*/
const updateDatasetDeprecationByUrn = (
urn: string,
deprecated: boolean,
deprecationNote: string = ''
): Promise<void> => {
return putJSON<void>({
url: datasetDeprecationUrlByUrn(urn),
data: {
deprecated,
deprecationNote
}
});
};
export {
readDatasetProperties,
readNonPinotProperties,
readPinotProperties,
updateDatasetDeprecation,
updateDatasetDeprecationByUrn
};

View File

@ -0,0 +1,22 @@
import { IDatasetSchema, IDatasetSchemaGetResponse } from 'wherehows-web/typings/api/datasets/schema';
import { datasetUrlByUrn } from 'wherehows-web/utils/api/datasets/shared';
import { getJSON } from 'wherehows-web/utils/api/fetcher';
/**
* Returns the url for a dataset schema by urn
* @param {string} urn
* @return {string}
*/
const datasetSchemaUrlByUrn = (urn: string): string => `${datasetUrlByUrn(urn)}/schema`;
/**
* Reads the schema for a dataset with the related urn
* @param {string} urn
* @return {Promise<IDatasetSchema>}
*/
const readDatasetSchemaByUrn = async (urn: string): Promise<IDatasetSchema> => {
const { schema } = await getJSON<IDatasetSchemaGetResponse>({ url: datasetSchemaUrlByUrn(urn) });
return schema;
};
export { readDatasetSchemaByUrn };

View File

@ -19,9 +19,8 @@ export const datasetUrlById = (id: number): string => `${datasetsUrlRoot('v1')}/
* @param {string} urn * @param {string} urn
* @returns {string} * @returns {string}
*/ */
export const datasetUrlByUrn = (urn: string): string => `${getApiRoot('v2')}/dataset/${urn}`; export const datasetUrlByUrn = (urn: string): string => `${datasetsUrlRoot('v2')}/${urn}`;
//FIXME api plurality ^^^^^^^^^^
// export const datasetUrlByUrn = (urn: string): string => `${datasetsUrlRoot('v2')}/${urn}`;
/** /**
* Composes the datasets count url from a given platform and or prefix if provided * Composes the datasets count url from a given platform and or prefix if provided
* @param {Partial<IReadDatasetsOptionBag>} [{ platform, prefix }={}] * @param {Partial<IReadDatasetsOptionBag>} [{ platform, prefix }={}]

View File

@ -1,19 +1,24 @@
import { DatasetClassifiers } from 'wherehows-web/constants/dataset-classification'; import { DatasetClassifiers } from 'wherehows-web/constants/dataset-classification';
import { lastSeenSuggestionInterval } from 'wherehows-web/constants/metadata-acquisition'; import { lastSeenSuggestionInterval } from 'wherehows-web/constants/metadata-acquisition';
import { assert, warn } from '@ember/debug'; import { assert, warn } from '@ember/debug';
import { decodeUrn } from 'wherehows-web/utils/validators/urn';
/** /**
* Builds a default shape for securitySpecification & privacyCompliancePolicy with default / unset values * Builds a default shape for securitySpecification & privacyCompliancePolicy with default / unset values
* for non null properties as per Avro schema * for non null properties as per Avro schema
* @param {number} datasetId id for the dataset that this privacy object applies to * @param {number} datasetId identifier for the dataset that this privacy object applies to
*/ */
const createInitialComplianceInfo = datasetId => ({ const createInitialComplianceInfo = datasetId => {
datasetId, const identifier = typeof datasetId === 'string' ? { urn: decodeUrn(datasetId) } : { datasetId };
complianceType: '',
compliancePurgeNote: '', return {
complianceEntities: [], ...identifier,
datasetClassification: {} complianceType: '',
}); compliancePurgeNote: '',
complianceEntities: [],
datasetClassification: {}
};
};
/** /**
* *

View File

@ -1,33 +0,0 @@
import { urnRegex } from 'wherehows-web/utils/validators/urn';
/**
* Takes a urn string and parse it into an array of breadcrumb objects with crumb, and urn as
* properties.
* Hierarchy is implied in element ordering
* @param {String} urn
* @return {Array.<{crumb, urn}>|null}
*/
export default urn => {
const urnMatch = urnRegex.exec(urn);
if (urnMatch) {
// Initial element in a match array from RegExp#exec is the full match, not needed here
const urnParts = urnMatch.filter((match, index) => index);
// Splits the 2nd captured group into an array of urn names and spreads into a new list
const crumbs = [urnParts[0], ...urnParts[1].split('/')];
// Reduces the crumbs into a list of crumb names and urn paths
return crumbs.reduce((breadcrumbs, crumb, index) => {
const previousCrumb = breadcrumbs[index - 1];
const breadcrumb = {
crumb,
// First item is root
urn: !index ? `${crumb}:///` : `${previousCrumb.urn}${crumb}/`
};
return [...breadcrumbs, breadcrumb];
}, []);
}
return null;
};

View File

@ -0,0 +1,41 @@
import { datasetUrnRegexWH } from 'wherehows-web/utils/validators/urn';
interface IBreadCrumb {
crumb: string;
urn: string;
}
/**
* Takes a urn string and parse it into an array of breadcrumb objects with crumb, and urn as
* properties.
* Hierarchy is implied in element ordering
* @param {String} urn
* @return {Array.<{crumb, urn}>|null}
*/
export default (urn: string): Array<{ crumb: string; urn: string }> | null => {
const urnMatch = datasetUrnRegexWH.exec(urn);
if (urnMatch) {
// Initial element in a match array from RegExp#exec is the full match, not needed here
const urnParts = urnMatch.filter((_match, index) => index);
// Splits the 2nd captured group into an array of urn names and spreads into a new list
const crumbs = [urnParts[0], ...urnParts[1].split('/')];
// Reduces the crumbs into a list of crumb names and urn paths
return crumbs.reduce(
(breadcrumbs, crumb, index) => {
const previousCrumb = breadcrumbs[index - 1];
const breadcrumb: IBreadCrumb = {
crumb,
// First item is root
urn: !index ? `${crumb}:///` : `${previousCrumb.urn}${crumb}/`
};
return [...breadcrumbs, breadcrumb];
},
<Array<IBreadCrumb>>[]
);
}
return null;
};

View File

@ -1,17 +1,19 @@
import { assert } from '@ember/debug';
/** /**
* Matches a url string with a `urn` query. urn query with letters or underscore segment of any length greater * Matches a url string with a `urn` query. urn query with letters or underscore segment of any length greater
* than 1 followed by colon and 3 forward slashes and a segment containing letters, {, }, _ or /, or none * than 1 followed by colon and 3 forward slashes and a segment containing letters, {, }, _ or /, or none
* The value following the urn key is retained * The value following the urn key is retained
* @type {RegExp} * @type {RegExp}
*/ */
const datasetUrnRegexWH = /([a-z_]+):\/{3}([a-z0-9_\-/{}]*)/i; const datasetUrnRegexWH = /([a-z_]+):\/{3}([a-z0-9_\-/{}.]*)/i;
/** /**
* Matches a urn string that follows the pattern captures, the comma delimited platform, segment and fabric * Matches a urn string that follows the pattern captures, the comma delimited platform, segment and fabric
* e.g urn:li:dataset:(urn:li:dataPlatform:PLATFORM,SEGMENT,FABRIC) * e.g urn:li:dataset:(urn:li:dataPlatform:PLATFORM,SEGMENT,FABRIC)
* @type {RegExp} * @type {RegExp}
*/ */
const datasetUrnRegexLI = /urn:li:dataset:\(urn:li:dataPlatform:(\w+),([\w.\-]+),(\w+)\)/; const datasetUrnRegexLI = /urn:li:dataset:\(urn:li:dataPlatform:(\w+),([\w.\-\/]+),(\w+)\)/;
/** /**
* Matches urn's that occur in flow urls * Matches urn's that occur in flow urls
@ -53,6 +55,61 @@ const getPlatformFromUrn = (candidateUrn: string) => {
} }
}; };
/**
* Converts a WH URN format to a LI URN format
* @param {string} whUrn
* @return {string}
*/
const convertWhUrnToLiUrn = (whUrn: string): string => {
assert(`Expected ${whUrn} to be in the WH urn format`, isWhUrn(whUrn));
const [, platform, path] = datasetUrnRegexWH.exec(whUrn)!;
return `urn:li:dataset:(urn:li:dataPlatform:${platform},${path},PROD)`;
};
/**
* Cached RegExp object for a global search of /
* @type {RegExp}
*/
const encodedSlashRegExp = new RegExp(encodeURIComponent('/'), 'g');
/**
* Replaces any occurrence of / with the encoded equivalent
* @param {string} urn
* @return {string}
*/
const encodeForwardSlash = (urn: string): string => urn.replace(/\//g, encodeURIComponent('/'));
/**
* Replaces encoded slashes with /
* @param {string} urn
* @return {string}
*/
const decodeForwardSlash = (urn: string): string => urn.replace(encodedSlashRegExp, decodeURIComponent('/'));
/**
* Replaces occurrences of / with the encoded counterpart in a urn string
* @param {string} urn
* @return {string}
*/
const encodeUrn = (urn: string): string => encodeForwardSlash(urn);
/**
* Replaces encoded occurrences of / with the string /
* @param {string} urn
* @return {string}
*/
const decodeUrn = (urn: string): string => decodeForwardSlash(urn);
export default isUrn; export default isUrn;
export { datasetUrnRegexWH, datasetUrnRegexLI, isWhUrn, isLiUrn, specialFlowUrnRegex, getPlatformFromUrn }; export {
datasetUrnRegexWH,
datasetUrnRegexLI,
isWhUrn,
isLiUrn,
specialFlowUrnRegex,
getPlatformFromUrn,
convertWhUrnToLiUrn,
encodeUrn,
decodeUrn
};

View File

@ -1,7 +1,7 @@
import { faker } from 'ember-cli-mirage'; import { faker } from 'ember-cli-mirage';
import { IFunctionRouteHandler, IMirageServer } from 'wherehows-web/typings/ember-cli-mirage'; import { IFunctionRouteHandler, IMirageServer } from 'wherehows-web/typings/ember-cli-mirage';
import { ApiStatus } from 'wherehows-web/utils/api/shared'; import { ApiStatus } from 'wherehows-web/utils/api/shared';
import { getDatasetColumns } from 'wherehows-web/mirage/helpers/columns'; import { getDatasetColumns, getDatasetSchema } from 'wherehows-web/mirage/helpers/columns';
import { getDatasetCompliance } from 'wherehows-web/mirage/helpers/compliance'; import { getDatasetCompliance } from 'wherehows-web/mirage/helpers/compliance';
import { getComplianceDataTypes } from 'wherehows-web/mirage/helpers/compliance-data-types'; import { getComplianceDataTypes } from 'wherehows-web/mirage/helpers/compliance-data-types';
import { getDatasetComplianceSuggestion } from 'wherehows-web/mirage/helpers/compliance-suggestions'; import { getDatasetComplianceSuggestion } from 'wherehows-web/mirage/helpers/compliance-suggestions';
@ -32,6 +32,16 @@ export default function(this: IMirageServer) {
this.namespace = '/api/v2'; this.namespace = '/api/v2';
this.get('/datasets/:identifier/', getDatasetView);
this.get('/datasets/:identifier/owners', getDatasetOwners);
this.get('/datasets/:dataset_id/schema', getDatasetSchema);
this.get('/datasets/:dataset_id/compliance/suggestions', getDatasetComplianceSuggestion);
this.get('/datasets/:dataset_id/owners', getDatasetOwners);
this.get('/list/complianceDataTypes', getComplianceDataTypes); this.get('/list/complianceDataTypes', getComplianceDataTypes);
this.get('/list/platforms', getDatasetPlatforms); this.get('/list/platforms', getDatasetPlatforms);

View File

@ -0,0 +1,3 @@
const urn = 'urn:li:dataset:(urn:li:dataPlatform:hdfs,%2Fjobs%2Faffinity%2FeAffinity%2Fmaster%2Fold-job-seeker,PROD)';
export { urn };

View File

@ -9,4 +9,15 @@ const getDatasetColumns = function(this: IFunctionRouteHandler, { columns }: { c
}; };
}; };
export { getDatasetColumns }; const getDatasetSchema = function(this: IFunctionRouteHandler, { columns }: { columns: any }) {
return {
schema: {
columns: this.serialize(columns.all()),
schemaless: false,
keySchema: null,
rawSchema: '{}'
}
};
};
export { getDatasetColumns, getDatasetSchema };

View File

@ -1,10 +1,8 @@
import { IFunctionRouteHandler } from 'wherehows-web/typings/ember-cli-mirage'; import { IFunctionRouteHandler } from 'wherehows-web/typings/ember-cli-mirage';
import { ApiStatus } from 'wherehows-web/utils/api/shared';
const getDatasetOwners = function(this: IFunctionRouteHandler, { owners }: { owners: any }) { const getDatasetOwners = function(this: IFunctionRouteHandler, { owners }: { owners: any }) {
return { return {
owners: this.serialize(owners.all()), owners: this.serialize(owners.all())
status: ApiStatus.OK
}; };
}; };

View File

@ -1,10 +1,8 @@
import { IFunctionRouteHandler } from 'wherehows-web/typings/ember-cli-mirage'; import { IFunctionRouteHandler } from 'wherehows-web/typings/ember-cli-mirage';
import { ApiStatus } from 'wherehows-web/utils/api/shared';
const getDatasetView = function(this: IFunctionRouteHandler, { datasetViews }: { datasetViews: any }) { const getDatasetView = function(this: IFunctionRouteHandler, { datasetViews }: { datasetViews: any }) {
return { return {
dataset: this.serialize(datasetViews.first()), dataset: this.serialize(datasetViews.first())
status: ApiStatus.OK
}; };
}; };

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
@ -10,7 +10,8 @@ moduleForAcceptance('Acceptance | datasets/dataset/comments', {
} }
}); });
test('visiting /datasets/dataset/comments', async function(assert) { // feature not yet supported in v2
skip('visiting /datasets/dataset/comments', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/comments'; const url = '/datasets/12345/comments';

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
@ -10,7 +10,7 @@ moduleForAcceptance('Acceptance | datasets/dataset/ownership', {
} }
}); });
test('visiting /datasets/dataset/ownership', async function(assert) { skip('visiting /datasets/dataset/ownership', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/ownership'; const url = '/datasets/12345/ownership';

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
@ -10,7 +10,7 @@ moduleForAcceptance('Acceptance | datasets/dataset/properties', {
} }
}); });
test('visiting /datasets/dataset/properties', async function(assert) { skip('visiting /datasets/dataset/properties', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/properties'; const url = '/datasets/12345/properties';

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
@ -10,7 +10,7 @@ moduleForAcceptance('Acceptance | datasets/dataset/relations', {
} }
}); });
test('visiting /datasets/dataset/relations', async function(assert) { skip('visiting /datasets/dataset/relations', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/relations'; const url = '/datasets/12345/relations';

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
@ -10,7 +10,7 @@ moduleForAcceptance('Acceptance | datasets/dataset/sample', {
} }
}); });
test('visiting /datasets/dataset/sample', async function(assert) { skip('visiting /datasets/dataset/sample', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/sample'; const url = '/datasets/12345/sample';

View File

@ -1,4 +1,4 @@
import { test } from 'qunit'; import { skip } from 'qunit';
import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance'; import moduleForAcceptance from 'wherehows-web/tests/helpers/module-for-acceptance';
import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers'; import { visit, find, currentURL, waitUntil } from 'ember-native-dom-helpers';
import defaultScenario from 'wherehows-web/mirage/scenarios/default'; import defaultScenario from 'wherehows-web/mirage/scenarios/default';
@ -10,7 +10,7 @@ moduleForAcceptance('Acceptance | datasets/dataset/schema', {
} }
}); });
test('visiting /datasets/dataset/schema', async function(assert) { skip('visiting /datasets/dataset/schema', async function(assert) {
assert.expect(2); assert.expect(2);
defaultScenario(server); defaultScenario(server);
const url = '/datasets/12345/schema'; const url = '/datasets/12345/schema';

View File

@ -23,7 +23,7 @@ test('it renders', function(assert) {
this.$() this.$()
.text() .text()
.trim(), .trim(),
'Dataset is deprecated?', 'Is this dataset deprecated?',
'shows the question asking if the dataset is deprecated' 'shows the question asking if the dataset is deprecated'
); );
assert.equal(this.$('#dataset-is-deprecated').length, 1, 'has one input checkbox with known selector'); assert.equal(this.$('#dataset-is-deprecated').length, 1, 'has one input checkbox with known selector');

View File

@ -0,0 +1,54 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { waitUntil, find } from 'ember-native-dom-helpers';
import { urn } from 'wherehows-web/mirage/fixtures/urn';
import sinon from 'sinon';
moduleForComponent(
'datasets/containers/dataset-compliance',
'Integration | Component | datasets/containers/dataset compliance',
{
integration: true,
beforeEach() {
this.server = sinon.createFakeServer();
},
afterEach() {
this.server.restore();
}
}
);
test('it renders', async function(assert) {
this.set('urn', urn);
this.server.respondWith('GET', /\/api\/v2\/datasets\/.*/, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({})
]);
this.server.respondWith(/.*\/complianceDataTypes/, [200, { 'Content-Type': 'application/json' }, JSON.stringify([])]);
this.server.respondWith(/.*\/complianceSuggestion/, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({})
]);
this.server.respondWith(/.*\/schema/, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({
schema: {
schemaless: false,
columns: [],
rawSchema: null,
keySchema: null
}
})
]);
this.render(hbs`{{datasets/containers/dataset-compliance urn=urn}}`);
this.server.respond();
await waitUntil(() => find('.compliance-container'));
assert.ok(document.querySelector('empty-state'), 'renders the empty state component');
});

View File

@ -0,0 +1,46 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { waitUntil, find } from 'ember-native-dom-helpers';
import { urn } from 'wherehows-web/mirage/fixtures/urn';
import sinon from 'sinon';
moduleForComponent(
'datasets/containers/dataset-ownership',
'Integration | Component | datasets/containers/dataset ownership',
{
integration: true,
beforeEach() {
this.server = sinon.createFakeServer();
this.server.respondImmediately = true;
},
afterEach() {
this.server.restore();
}
}
);
test('it renders', async function(assert) {
const lookupClass = '.dataset-author-user-lookup';
this.set('urn', urn);
this.server.respondWith('GET', /\/api\/v2\/datasets.*/, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({})
]);
this.server.respondWith('GET', '/api/v1/owner/types', [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify([])
]);
this.render(hbs`{{datasets/containers/dataset-ownership urn=urn}}`);
await waitUntil(() => find(lookupClass));
assert.equal(
document.querySelector(lookupClass).textContent.trim(),
'Add an Owner',
'shows dataset authors component'
);
});

View File

@ -0,0 +1,35 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { waitUntil, find } from 'ember-native-dom-helpers';
import { urn } from 'wherehows-web/mirage/fixtures/urn';
moduleForComponent(
'datasets/containers/dataset-properties',
'Integration | Component | datasets/containers/dataset properties',
{
integration: true,
beforeEach() {
this.server = sinon.createFakeServer();
this.server.respondImmediately = true;
},
afterEach() {
this.server.restore();
}
}
);
test('it renders', function(assert) {
const labelClass = '.dataset-deprecation-toggle__toggle-header__label';
this.set('urn', urn);
this.server.respondWith('GET', /\/api\/v2\/datasets.*/, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({})
]);
this.render(hbs`{{datasets/containers/dataset-properties urn=urn}}`);
assert.equal(find(labelClass).textContent.trim(), 'Is this dataset deprecated?', 'renders presentation component');
});

View File

@ -0,0 +1,28 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { find } from 'ember-native-dom-helpers';
import { urn } from 'wherehows-web/mirage/fixtures/urn';
import sinon from 'sinon';
moduleForComponent(
'datasets/containers/dataset-schema',
'Integration | Component | datasets/containers/dataset schema',
{
integration: true,
beforeEach() {
this.server = sinon.createFakeServer();
},
afterEach() {
this.server.restore();
}
}
);
test('it renders', function(assert) {
this.set('urn', urn);
this.render(hbs`{{datasets/containers/dataset-schema urn=urn}}`);
assert.ok(find('#json-viewer'), 'renders the dataset schema component');
});