mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-14 18:38:27 +00:00
adds feature to download a compliance policy as a json document. upload as json and apply to policy. performs attribute type assertions on values in uploaded policy
This commit is contained in:
parent
30f9bf28b2
commit
c3de4a5ff7
@ -1,9 +1,7 @@
|
|||||||
import Base from 'ember-simple-auth/authenticators/base';
|
import Base from 'ember-simple-auth/authenticators/base';
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
const {
|
const { $: { post } } = Ember;
|
||||||
$: { post }
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Base.extend({
|
export default Base.extend({
|
||||||
/**
|
/**
|
||||||
@ -11,7 +9,7 @@ export default Base.extend({
|
|||||||
* Resolves with data object returned from successful request.
|
* Resolves with data object returned from successful request.
|
||||||
* @param {String} username username to authenticate with
|
* @param {String} username username to authenticate with
|
||||||
* @param {String} password matching candidate password for username
|
* @param {String} password matching candidate password for username
|
||||||
* @return {Promise<{Object, String}>}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
authenticate: (username, password) =>
|
authenticate: (username, password) =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
|
@ -8,8 +8,19 @@ import {
|
|||||||
nonIdFieldLogicalTypes,
|
nonIdFieldLogicalTypes,
|
||||||
defaultFieldDataTypeClassification
|
defaultFieldDataTypeClassification
|
||||||
} from 'wherehows-web/constants';
|
} from 'wherehows-web/constants';
|
||||||
|
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions';
|
||||||
|
|
||||||
const { Component, computed, set, get, setProperties, getWithDefault, isEmpty, String: { htmlSafe } } = Ember;
|
const {
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
set,
|
||||||
|
get,
|
||||||
|
setProperties,
|
||||||
|
getProperties,
|
||||||
|
getWithDefault,
|
||||||
|
isEmpty,
|
||||||
|
String: { htmlSafe }
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
// TODO: DSS-6671 Extract to constants module
|
// TODO: DSS-6671 Extract to constants module
|
||||||
const missingTypes = 'Looks like some fields may contain privacy data but do not have a specified `Field Format`?';
|
const missingTypes = 'Looks like some fields may contain privacy data but do not have a specified `Field Format`?';
|
||||||
@ -153,6 +164,13 @@ export default Component.extend({
|
|||||||
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
|
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches the policy's modification time in milliseconds
|
||||||
|
*/
|
||||||
|
policyModificationTimeInEpoch: computed('complianceInfo', function() {
|
||||||
|
return getWithDefault(this, 'complianceInfo.modifiedTime', 0) * 1000;
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
|
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
|
||||||
* tracking header.
|
* tracking header.
|
||||||
@ -405,6 +423,38 @@ export default Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
/**
|
||||||
|
* Receives the json representation for compliance and applies each key to the policy
|
||||||
|
* @param {String} textString string representation for the JSON file
|
||||||
|
*/
|
||||||
|
onComplianceJsonUpload(textString) {
|
||||||
|
const policy = JSON.parse(textString);
|
||||||
|
if (isPolicyExpectedShape(policy)) {
|
||||||
|
const currentPolicy = get(this, 'complianceInfo');
|
||||||
|
return set(this, 'complianceInfo', Object.assign({}, currentPolicy, policy));
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Received policy in an unexpected format! Please check the provided attributes and try again.');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the compliance policy download action
|
||||||
|
*/
|
||||||
|
onComplianceDownloadJson() {
|
||||||
|
const currentPolicy = get(this, 'complianceInfo');
|
||||||
|
const policyProps = [
|
||||||
|
datasetClassificationKey,
|
||||||
|
policyFieldClassificationKey,
|
||||||
|
policyComplianceEntitiesKey
|
||||||
|
].map(name => name.split('.').pop());
|
||||||
|
const policy = Object.assign({}, getProperties(currentPolicy, policyProps));
|
||||||
|
const href = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(policy))}`;
|
||||||
|
const download = `${get(this, 'datasetName')}_policy.json`;
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
Object.assign(anchor, { download, href });
|
||||||
|
anchor.click();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a user updates the identifierFieldType in the DOM, update the backing store
|
* When a user updates the identifierFieldType in the DOM, update the backing store
|
||||||
* @param {String} identifierField
|
* @param {String} identifierField
|
||||||
|
51
wherehows-web/app/components/json-upload.js
Normal file
51
wherehows-web/app/components/json-upload.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const { get, computed, Component } = Ember;
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
classNames: ['nacho-uploader'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the change event on the json-upload DOM element
|
||||||
|
* @param {FileList} files extracts a FileList object from the event object
|
||||||
|
* @return {void|null}
|
||||||
|
*/
|
||||||
|
change({ target: { files = [] } }) {
|
||||||
|
const hasFiles = !!files.length;
|
||||||
|
if (hasFiles) {
|
||||||
|
return this.sendFileAsText(Array.from(files).shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches a unique id for the HTML Element for file upload
|
||||||
|
*/
|
||||||
|
uploadInputId: computed(function() {
|
||||||
|
return `${get(this, 'elementId')}-uploader`;
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a file as a text string and passes the output to the closure action
|
||||||
|
* @param {Blob} fileBlob
|
||||||
|
*/
|
||||||
|
sendFileAsText(fileBlob) {
|
||||||
|
const closureAction = get(this, 'attrs.receiveJsonFile');
|
||||||
|
const reader = new FileReader();
|
||||||
|
if (typeof closureAction === 'function') {
|
||||||
|
reader.onload = ({ target: { result } }) => closureAction(result);
|
||||||
|
reader.readAsText(fileBlob);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* Proxies the user's click interaction on the styled upload button to the file input element
|
||||||
|
*/
|
||||||
|
didSelectUpload() {
|
||||||
|
const uploadInput = this.$(`#${get(this, 'uploadInputId')}`);
|
||||||
|
return uploadInput.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -52,7 +52,8 @@ $color-scheme: (
|
|||||||
),
|
),
|
||||||
blue: (
|
blue: (
|
||||||
oxford: rgb(53, 75, 87),
|
oxford: rgb(53, 75, 87),
|
||||||
curious: rgb(26, 161, 217)
|
curious: rgb(26, 161, 217),
|
||||||
|
eastern: rgb(26, 132, 188)
|
||||||
),
|
),
|
||||||
grey: (
|
grey: (
|
||||||
light: rgb(237, 237, 237),
|
light: rgb(237, 237, 237),
|
||||||
|
@ -16,3 +16,4 @@
|
|||||||
@import "nacho/nacho-select";
|
@import "nacho/nacho-select";
|
||||||
@import "nacho/nacho-pager";
|
@import "nacho/nacho-pager";
|
||||||
@import "nacho/nacho-breadcrumbs";
|
@import "nacho/nacho-breadcrumbs";
|
||||||
|
@import "nacho/nacho-uploader";
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
@import "../abstracts/functions";
|
@import "../abstracts/functions";
|
||||||
@import "../abstracts/mixins";
|
@import "../abstracts/mixins";
|
||||||
|
|
||||||
|
$item-spacing: 10px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override Bootstrap navigation class, .navbar, ruleset
|
* Override Bootstrap navigation class, .navbar, ruleset
|
||||||
*/
|
*/
|
||||||
@ -51,6 +54,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styles the non primary action bar for an entity
|
||||||
|
*/
|
||||||
|
.secondary-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__action + &__action {
|
||||||
|
margin-left: $item-spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styles the action bar on a tab component
|
* Styles the action bar on a tab component
|
||||||
*/
|
*/
|
||||||
@ -64,6 +80,15 @@
|
|||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
z-index: z(dropdown);
|
z-index: z(dropdown);
|
||||||
max-height: 50px;
|
max-height: 50px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item + &__item {
|
||||||
|
margin-left: $item-spacing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,3 +39,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.policy-last-saved {
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
&__saved {
|
||||||
|
font-weight: fw(normal, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,17 +6,20 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$main-button-color: set-color(blue, eastern);
|
||||||
|
$button-text-color: set-color(white, base);
|
||||||
|
|
||||||
@include restyle-define(button, (
|
@include restyle-define(button, (
|
||||||
// Button Component Variables
|
// Button Component Variables
|
||||||
restyle-var(height): 32px,
|
restyle-var(height): 32px,
|
||||||
restyle-var(horizontal-padding): 16px,
|
restyle-var(horizontal-padding): 16px,
|
||||||
restyle-var(main-color): $secondary-color,
|
restyle-var(main-color): $main-button-color,
|
||||||
restyle-var(background-color): $secondary-color,
|
restyle-var(background-color): $main-button-color,
|
||||||
restyle-var(font-size): 1.7rem,
|
restyle-var(font-size): 1.7rem,
|
||||||
restyle-var(font-weight): 600,
|
restyle-var(font-weight): 600,
|
||||||
restyle-var(text-color): $text-color,
|
restyle-var(text-color): $button-text-color,
|
||||||
restyle-var(border-radius): 2px,
|
restyle-var(border-radius): 2px,
|
||||||
restyle-var(disabled-text): tint($text-color, 70%),
|
restyle-var(disabled-text): tint($button-text-color, 70%),
|
||||||
|
|
||||||
background-color: restyle-var(background-color),
|
background-color: restyle-var(background-color),
|
||||||
border: 0,
|
border: 0,
|
||||||
@ -50,7 +53,7 @@
|
|||||||
|
|
||||||
restyle-modifiers: (
|
restyle-modifiers: (
|
||||||
'with a border': (
|
'with a border': (
|
||||||
border: 1px solid restyle-var(text-color)
|
border: 1px solid restyle-var(main-color)
|
||||||
),
|
),
|
||||||
small: (
|
small: (
|
||||||
restyle-var(font-size): 1.5rem,
|
restyle-var(font-size): 1.5rem,
|
||||||
@ -76,7 +79,7 @@
|
|||||||
),
|
),
|
||||||
secondary: (
|
secondary: (
|
||||||
restyle-var(background-color): transparent,
|
restyle-var(background-color): transparent,
|
||||||
restyle-var(text-color): $text-invert-color,
|
restyle-var(text-color): $main-button-color,
|
||||||
restyle-var(box-shadow-size): inset 0 0 0 1px,
|
restyle-var(box-shadow-size): inset 0 0 0 1px,
|
||||||
restyle-var(box-shadow-size-hover): inset 0 0 0 2px,
|
restyle-var(box-shadow-size-hover): inset 0 0 0 2px,
|
||||||
restyle-var(box-shadow-size-active): inset 0 0 0 3px,
|
restyle-var(box-shadow-size-active): inset 0 0 0 3px,
|
||||||
|
@ -18,4 +18,10 @@ $height: 40px;
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
border: 1px solid $secondary-color;
|
||||||
|
background-color: $secondary-color;
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.nacho-uploader {
|
||||||
|
@include flex-center;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
width: 250px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -3,4 +3,5 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="tab-body">
|
<div class="tab-body">
|
||||||
<section class="action-bar">
|
<section class="action-bar">
|
||||||
<div class="container">
|
<div class="container action-bar__content">
|
||||||
<button class="nacho-button nacho-button--large-inverse"
|
<button class="nacho-button nacho-button--large-inverse action-bar__item"
|
||||||
title={{if ownershipIsInvalid
|
title={{if ownershipIsInvalid
|
||||||
"Need at least two confirmed owners to make changes
|
"Need at least two confirmed owners to make changes
|
||||||
and no Invalid users"
|
and no Invalid users"
|
||||||
@ -11,7 +11,7 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="nacho-button nacho-button--large"
|
<button class="nacho-button nacho-button--large action-bar__item"
|
||||||
{{action "addOwner" owners}}>
|
{{action "addOwner" owners}}>
|
||||||
<i class="fa fa-plus" title="Add an Owner">
|
<i class="fa fa-plus" title="Add an Owner">
|
||||||
</i>
|
</i>
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<section class="action-bar">
|
<section class="action-bar">
|
||||||
<div class="container">
|
<div class="container action-bar__content">
|
||||||
<button class="nacho-button nacho-button--large-inverse"
|
<button class="nacho-button nacho-button--large-inverse action-bar__item"
|
||||||
title={{unless isDatasetFullyClassified
|
title={{unless isDatasetFullyClassified
|
||||||
"Ensure you have provided a yes/no value for all dataset tags"
|
"Ensure you have provided a yes/no value for all dataset tags"
|
||||||
"Save"}}
|
"Save"}}
|
||||||
@ -23,7 +23,7 @@
|
|||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="nacho-button nacho-button--large"
|
<button class="nacho-button nacho-button--large action-bar__item"
|
||||||
{{action "resetCompliance"}}>
|
{{action "resetCompliance"}}>
|
||||||
<i class="fa fa-times" title="Start Over">
|
<i class="fa fa-times" title="Start Over">
|
||||||
</i>
|
</i>
|
||||||
@ -39,6 +39,25 @@
|
|||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="secondary-actions">
|
||||||
|
<div class="policy-last-saved">
|
||||||
|
Last saved:
|
||||||
|
<span class="policy-last-saved__saved">
|
||||||
|
{{if isNewPrivacyPolicy 'Never'
|
||||||
|
(moment-calendar policyModificationTimeInEpoch sameElse="MMM Do YYYY, h:mm a")}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}}
|
||||||
|
|
||||||
|
{{#unless isNewPrivacyPolicy}}
|
||||||
|
<button
|
||||||
|
{{action "onComplianceDownloadJson"}}
|
||||||
|
class="nacho-button nacho-button--large-inverse secondary-actions__action">
|
||||||
|
Download as a JSON file
|
||||||
|
</button>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{#if isNewPrivacyPolicy}}
|
{{#if isNewPrivacyPolicy}}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<p>
|
<p>
|
||||||
|
16
wherehows-web/app/templates/components/json-upload.hbs
Normal file
16
wherehows-web/app/templates/components/json-upload.hbs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
style="display:none"
|
||||||
|
id="{{uploadInputId}}">
|
||||||
|
|
||||||
|
<label for="{{concat elementId '-button'}}" class="nacho-uploader__label">
|
||||||
|
Have existing metadata from a similar dataset that you want to apply here?
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{{action "didSelectUpload"}}
|
||||||
|
id="{{concat elementId '-button'}}"
|
||||||
|
class="nacho-button nacho-button--large-inverse">
|
||||||
|
Choose File
|
||||||
|
</button>
|
@ -3,7 +3,7 @@
|
|||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-default dropdown-toggle nacho-button--large"
|
class="btn btn-default dropdown-toggle nacho-button--large search-button"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
@ -38,7 +38,7 @@
|
|||||||
class="form-control nacho-global-search__text-input"}}
|
class="form-control nacho-global-search__text-input"}}
|
||||||
|
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-default nacho-button--large"
|
<button class="btn btn-default nacho-button--large search-button"
|
||||||
id="global-search-button"
|
id="global-search-button"
|
||||||
type="submit" {{action "search"}}>
|
type="submit" {{action "search"}}>
|
||||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { datasetClassifiers } from 'wherehows-web/constants/dataset-classification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@ -15,4 +17,78 @@ const createInitialComplianceInfo = datasetId => ({
|
|||||||
retentionPolicy: { retentionType: '' }
|
retentionPolicy: { retentionType: '' }
|
||||||
});
|
});
|
||||||
|
|
||||||
export { createInitialComplianceInfo };
|
/**
|
||||||
|
*
|
||||||
|
* @type {{complianceEntities: {type: string, of: {type: string, keys: [*]}}, datasetClassification: {type: string, keys: (*)}, fieldClassification: {type: string}}}
|
||||||
|
*/
|
||||||
|
const policyShape = {
|
||||||
|
complianceEntities: {
|
||||||
|
type: 'array',
|
||||||
|
of: {
|
||||||
|
type: 'object',
|
||||||
|
keys: [
|
||||||
|
'identifierField:string',
|
||||||
|
'identifierType:string',
|
||||||
|
'isSubject:boolean|object',
|
||||||
|
'logicalType:string|object|undefined'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
datasetClassification: { type: 'object', keys: Object.keys(datasetClassifiers).map(key => `${key}:boolean`) },
|
||||||
|
fieldClassification: { type: 'object' }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that a policy is valid
|
||||||
|
* @param candidatePolicy
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
const isPolicyExpectedShape = (candidatePolicy = {}) => {
|
||||||
|
const candidateMatchesShape = policyKey => {
|
||||||
|
const policyProps = policyShape[policyKey];
|
||||||
|
const expectedType = policyProps.type;
|
||||||
|
const policyKeyValue = candidatePolicy[policyKey];
|
||||||
|
const isValueExpectedType = expectedType === 'array'
|
||||||
|
? Array.isArray(policyKeyValue)
|
||||||
|
: typeof policyKeyValue === expectedType;
|
||||||
|
const typeDeclarations = {
|
||||||
|
get array() {
|
||||||
|
return policyProps.of.keys;
|
||||||
|
},
|
||||||
|
get object() {
|
||||||
|
return policyProps.keys;
|
||||||
|
}
|
||||||
|
}[expectedType] || [];
|
||||||
|
|
||||||
|
if (!policyKeyValue || !isValueExpectedType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === 'array') {
|
||||||
|
return policyKeyValue.every(value => {
|
||||||
|
if (!value && typeof value !== policyProps.of.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return typeDeclarations.every(typeString => {
|
||||||
|
const [key, type] = typeString.split(':');
|
||||||
|
return type.includes(typeof value[key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === typeof {}) {
|
||||||
|
return typeDeclarations.every(typeString => {
|
||||||
|
const [key, type] = typeString.split(':');
|
||||||
|
return type.includes(typeof policyKeyValue[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof candidatePolicy === 'object' && candidatePolicy) {
|
||||||
|
return Object.keys(policyShape).every(candidateMatchesShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createInitialComplianceInfo, isPolicyExpectedShape };
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { moduleForComponent, test } from 'ember-qunit';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
|
moduleForComponent('json-upload', 'Integration | Component | json upload', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', function(assert) {
|
||||||
|
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
|
||||||
|
this.render(hbs`{{json-upload}}`);
|
||||||
|
|
||||||
|
assert.equal(this.$().text().trim(), '');
|
||||||
|
|
||||||
|
// Template block usage:
|
||||||
|
this.render(hbs`
|
||||||
|
{{#json-upload}}
|
||||||
|
template block text
|
||||||
|
{{/json-upload}}
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.equal(this.$().text().trim(), 'template block text');
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user