Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/app/components/config_details/config_details.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.config-root {
margin: 0;
}

.config-entry {
padding: 4px 0;
}

.config-entry-scalar {
display: flex;
flex-wrap: nowrap;
align-items: baseline;
}

.config-entry-scalar > .detail-label {
flex: 0 0 160px;
text-transform: uppercase;
}

.config-entry-scalar > .detail-value {
flex: 1 1 auto;
min-width: 0;
word-break: break-word;
}

.config-entry-nested > .detail-label {
text-transform: uppercase;
margin-bottom: 2px;
}

.config-nested {
margin: 0;
padding-left: 20px;
border-left: 1px solid #e0e0e0;
}

.config-nested .config-entry {
padding: 2px 0;
}

.config-nested .config-entry-scalar > .detail-label {
flex: 0 0 140px;
}

.config-entry code {
margin-right: 4px;
}

.config-array-item {
display: flex;
flex-wrap: nowrap;
align-items: baseline;
padding: 2px 0;
}
44 changes: 44 additions & 0 deletions src/app/components/config_details/config_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<div class="section-content">
<h6>Config</h6>
<div class="panel">
<div class="panel-body">
<script type="text/ng-template" id="config-entry.html">
<div ng-if="entry.type === 'scalar'" class="config-entry config-entry-scalar">
<dt class="detail-label" ng-bind="entry.key"></dt>
<dd class="detail-value" ng-bind="entry.value"></dd>
</div>
<div ng-if="entry.type === 'array'" class="config-entry config-entry-scalar">
<dt class="detail-label" ng-bind="entry.key"></dt>
<dd class="detail-value">
<span ng-if="entry.isSimpleArray">
<code ng-repeat="item in entry.value" ng-bind="item"></code>
</span>
<dl ng-if="!entry.isSimpleArray" class="config-nested">
<div ng-repeat="child in entry.value" class="config-array-item">
<dt class="detail-label">[<span ng-bind="$index"></span>]</dt>
<dd ng-if="child.type === 'scalar'" class="detail-value" ng-bind="child.value"></dd>
<dd ng-if="child.type === 'object'" class="detail-value">
<dl class="config-nested">
<div ng-repeat="nestedEntry in child.entries" ng-init="entry = nestedEntry" ng-include="'config-entry.html'"></div>
</dl>
</dd>
</div>
</dl>
</dd>
</div>
<div ng-if="entry.type === 'object'" class="config-entry config-entry-nested">
<dt class="detail-label" ng-bind="entry.key"></dt>
<dd class="detail-value">
<dl class="config-nested">
<div ng-repeat="nestedEntry in entry.entries" ng-init="entry = nestedEntry" ng-include="'config-entry.html'"></div>
</dl>
</dd>
</div>
</script>
<dl class="config-root">
<div ng-repeat="entry in entries" ng-include="'config-entry.html'"></div>
</dl>
<div ng-if="entries.length === 0" class="text-muted">No config available</div>
</div>
</div>
</div>
30 changes: 30 additions & 0 deletions src/app/components/config_details/config_details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const template = require('./config_details.html');
require("./config_details.css");

const _ = require('underscore');
const { toEntries } = require('./config_utils');

angular
.module('dbt')
.directive('configDetails', [function() {
return {
scope: {
config: '=',
},
templateUrl: template,
link: function(scope) {

scope.entries = [];

scope.$watch("config", function(nv) {
if (nv && _.isObject(nv)) {
scope.entries = toEntries(nv);
} else {
scope.entries = [];
}
});
}
}
}]);
63 changes: 63 additions & 0 deletions src/app/components/config_details/config_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

var _ = require('underscore');

var PRIORITY_KEYS = ['materialized', 'enabled', 'schema', 'database', 'alias', 'tags', 'group'];

function isEmptyValue(value) {
if (value === null || value === undefined) return true;
if (_.isArray(value) && value.length === 0) return true;
if (_.isObject(value) && !_.isArray(value) && _.isEmpty(value)) return true;
return false;
}

function isSimpleArray(arr) {
return _.every(arr, function(item) {
return !_.isObject(item);
});
}

function sortKeys(keys) {
return keys.sort(function(a, b) {
var aIdx = PRIORITY_KEYS.indexOf(a);
var bIdx = PRIORITY_KEYS.indexOf(b);
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
if (aIdx !== -1) return -1;
if (bIdx !== -1) return 1;
return a.localeCompare(b);
});
}

function toEntry(key, value) {
if (_.isArray(value)) {
if (isSimpleArray(value)) {
return { key: key, value: value, type: 'array', isSimpleArray: true };
}
var children = _.map(value, function(item) {
if (_.isObject(item)) {
return { type: 'object', entries: toEntries(item) };
}
return { type: 'scalar', value: item };
});
return { key: key, value: children, type: 'array', isSimpleArray: false };
}
if (_.isObject(value)) {
return { key: key, entries: toEntries(value), type: 'object' };
}
return { key: key, value: value, type: 'scalar' };
}

function toEntries(obj) {
var keys = sortKeys(_.keys(obj));
return _.chain(keys)
.filter(function(k) { return !isEmptyValue(obj[k]); })
.map(function(k) { return toEntry(k, obj[k]); })
.value();
}

module.exports = {
isEmptyValue: isEmptyValue,
isSimpleArray: isSimpleArray,
toEntry: toEntry,
toEntries: toEntries
};
169 changes: 169 additions & 0 deletions src/app/components/config_details/config_utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const { isEmptyValue, isSimpleArray, toEntry, toEntries } = require('./config_utils');

describe('config_utils', function() {

describe('isEmptyValue', function() {
it('returns true for null', function() {
expect(isEmptyValue(null)).toBe(true);
});

it('returns true for undefined', function() {
expect(isEmptyValue(undefined)).toBe(true);
});

it('returns true for empty array', function() {
expect(isEmptyValue([])).toBe(true);
});

it('returns true for empty object', function() {
expect(isEmptyValue({})).toBe(true);
});

it('returns false for non-empty string', function() {
expect(isEmptyValue('hello')).toBe(false);
});

it('returns false for zero', function() {
expect(isEmptyValue(0)).toBe(false);
});

it('returns false for false', function() {
expect(isEmptyValue(false)).toBe(false);
});

it('returns false for non-empty array', function() {
expect(isEmptyValue([1])).toBe(false);
});

it('returns false for non-empty object', function() {
expect(isEmptyValue({ a: 1 })).toBe(false);
});
});

describe('isSimpleArray', function() {
it('returns true for array of strings', function() {
expect(isSimpleArray(['a', 'b'])).toBe(true);
});

it('returns true for array of numbers', function() {
expect(isSimpleArray([1, 2])).toBe(true);
});

it('returns true for mixed primitives', function() {
expect(isSimpleArray(['a', 1, true])).toBe(true);
});

it('returns false for array containing objects', function() {
expect(isSimpleArray([{ a: 1 }])).toBe(false);
});

it('returns true for empty array', function() {
expect(isSimpleArray([])).toBe(true);
});
});

describe('toEntry', function() {
it('creates scalar entry for string', function() {
expect(toEntry('key', 'value')).toEqual({
key: 'key', value: 'value', type: 'scalar'
});
});

it('creates scalar entry for number', function() {
expect(toEntry('count', 42)).toEqual({
key: 'count', value: 42, type: 'scalar'
});
});

it('creates scalar entry for boolean', function() {
expect(toEntry('enabled', true)).toEqual({
key: 'enabled', value: true, type: 'scalar'
});

expect(toEntry('enabled', false)).toEqual({
key: 'enabled', value: false, type: 'scalar'
});
});

it('creates simple array entry', function() {
var result = toEntry('tags', ['a', 'b']);
expect(result.type).toBe('array');
expect(result.isSimpleArray).toBe(true);
expect(result.value).toEqual(['a', 'b']);
});

it('creates complex array entry', function() {
var result = toEntry('items', [{ name: 'x' }]);
expect(result.type).toBe('array');
expect(result.isSimpleArray).toBe(false);
expect(result.value[0].type).toBe('object');
});

it('creates object entry', function() {
var result = toEntry('contract', { enforced: true });
expect(result.type).toBe('object');
expect(result.entries).toEqual([
{ key: 'enforced', value: true, type: 'scalar' }
]);
});
});

describe('toEntries', function() {
it('filters out null values', function() {
var result = toEntries({ a: 'x', b: null, c: 'y' });
expect(result.map(function(e) { return e.key; })).toEqual(['a', 'c']);
});

it('filters out empty objects', function() {
var result = toEntries({ a: 'x', b: {} });
expect(result.map(function(e) { return e.key; })).toEqual(['a']);
});

it('filters out empty arrays', function() {
var result = toEntries({ a: 'x', b: [] });
expect(result.map(function(e) { return e.key; })).toEqual(['a']);
});

it('sorts priority keys first', function() {
var result = toEntries({
on_schema_change: 'ignore',
materialized: 'view',
enabled: true,
alias: 'foo'
});
var keys = result.map(function(e) { return e.key; });
expect(keys).toEqual(['materialized', 'enabled', 'alias', 'on_schema_change']);
});

it('sorts non-priority keys alphabetically', function() {
var result = toEntries({ zebra: 1, apple: 2 });
var keys = result.map(function(e) { return e.key; });
expect(keys).toEqual(['apple', 'zebra']);
});

it('handles nested objects', function() {
var result = toEntries({
contract: { enforced: false },
docs: { show: true, node_color: 'crimson' }
});
expect(result.length).toBe(2);
expect(result[0].type).toBe('object');
expect(result[0].entries[0].key).toBe('enforced');
expect(result[1].entries.map(function(e) { return e.key; })).toEqual(['node_color', 'show']);
});

it('returns empty array for empty object', function() {
expect(toEntries({})).toEqual([]);
});

it('preserves boolean false as value', function() {
var result = toEntries({ enforced: false });
expect(result[0].value).toBe(false);
});

it('preserves zero as value', function() {
var result = toEntries({ count: 0 });
expect(result[0].value).toBe(0);
});
});
});
1 change: 1 addition & 0 deletions src/app/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ require('./column_details/column_details.js');
require('./code_block/code_block.js');
require('./macro_arguments/');
require('./references/');
require('./config_details/config_details.js');
6 changes: 6 additions & 0 deletions src/app/docs/analysis.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ <h1>
<ul class="nav nav-tabs">
<li ui-sref-active='active'><a ui-sref="dbt.analysis({'#': 'description'})">Description</a></li>
<li ui-sref-active='active' ng-show = "parentsLength != 0"><a ui-sref="dbt.analysis({'#': 'depends_on'})">Depends On</a></li>
<li ui-sref-active='active' ng-show="model.config"><a ui-sref="dbt.analysis({'#': 'config'})">Config</a></li>
<li ui-sref-active='active'><a ui-sref="dbt.analysis({'#': 'sql'})">SQL</a></li>
</ul>
</div>
Expand Down Expand Up @@ -63,6 +64,11 @@ <h6>Depends On</h6>
</div>
</section>

<section class="section" ng-show="model.config">
<div class="section-target" id="config"></div>
<config-details config="model.config"></config-details>
</section>

<section class="section">
<div class="section-target" id="sql"></div>
<div class="section-content">
Expand Down
Loading