From 6ac463290efd0a0150889fd13c94594df1f1e224 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 8 Jul 2015 13:45:13 -0700 Subject: [PATCH] Add DB schema view beside query editor --- .../explorer-schema-onetable.js.es6 | 12 ++ .../components/explorer-schema.js.es6 | 108 ++++++++++++++++ .../routes/admin-plugins-explorer.js.es6 | 13 +- .../templates/admin/plugins-explorer-show.hbs | 48 +++++--- .../templates/admin/plugins-explorer.hbs | 2 +- .../components/explorer-schema-onetable.hbs | 35 ++++++ .../templates/components/explorer-schema.hbs | 7 ++ assets/stylesheets/explorer.scss | 72 +++++++++-- config/locales/client.en.yml | 5 + plugin.rb | 116 +++++++++++++++++- 10 files changed, 385 insertions(+), 33 deletions(-) create mode 100644 assets/javascripts/discourse/components/explorer-schema-onetable.js.es6 create mode 100644 assets/javascripts/discourse/components/explorer-schema.js.es6 create mode 100644 assets/javascripts/discourse/templates/components/explorer-schema-onetable.hbs create mode 100644 assets/javascripts/discourse/templates/components/explorer-schema.hbs diff --git a/assets/javascripts/discourse/components/explorer-schema-onetable.js.es6 b/assets/javascripts/discourse/components/explorer-schema-onetable.js.es6 new file mode 100644 index 0000000..4fa8f9b --- /dev/null +++ b/assets/javascripts/discourse/components/explorer-schema-onetable.js.es6 @@ -0,0 +1,12 @@ +export default Ember.Component.extend({ + classNameBindings: [':schema-table', 'open'], + + open: Em.computed.alias('table.open'), + + _bindClicks: function() { + const self = this; + this.$()./*children('.schema-table-name').*/click(function() { + self.set('open', !self.get('open')); + }); + }.on('didInsertElement') +}); diff --git a/assets/javascripts/discourse/components/explorer-schema.js.es6 b/assets/javascripts/discourse/components/explorer-schema.js.es6 new file mode 100644 index 0000000..2f01893 --- /dev/null +++ b/assets/javascripts/discourse/components/explorer-schema.js.es6 @@ -0,0 +1,108 @@ +export default Ember.Component.extend({ + + transformedSchema: function() { + const schema = this.get('schema'); + + for (let key in schema) { + if (!schema.hasOwnProperty(key)) { + continue; + } + + schema[key].forEach(function(col) { + let notes = false; + if (col.is_nullable) { + notes = "null"; + } + if (col.column_default) { + if (notes) { + notes += ", default " + col.column_default; + } else { + notes = "default " + col.column_default; + } + } + if (notes) { + col.notes = notes; + } + }); + } + return schema; + }.property('schema'), + + rfilter: function() { + if (!Em.isBlank(this.get('filter'))) { + return new RegExp(this.get('filter')); + } + }.property('filter'), + + filterTables: function(schema) { + let tables = []; + const filter = this.get('rfilter'), + haveFilter = !!filter; + + for (let key in schema) { + if (!schema.hasOwnProperty(key)) { + continue; + } + if (!haveFilter) { + tables.push({ + name: key, + columns: schema[key], + open: haveFilter + }); + continue; + } + + // Check the table name vs the filter + if (filter.source == key || filter.source + "s" == key) { + tables.unshift({ + name: key, + columns: schema[key], + open: haveFilter + }); + } else if (filter.test(key)) { + // whole table matches + tables.push({ + name: key, + columns: schema[key], + open: haveFilter + }); + } else { + // filter the columns + let filterCols = []; + schema[key].forEach(function(col) { + if (filter.source == col.column_name) { + filterCols.unshift(col); + } else if (filter.test(col.column_name)) { + filterCols.push(col); + } + }); + if (!Em.isEmpty(filterCols)) { + tables.push({ + name: key, + columns: filterCols, + open: haveFilter + }); + } + } + } + return tables; + }, + + triggerFilter: Discourse.debounce(function() { + this.set('filteredTables', this.filterTables(this.get('transformedSchema'))); + this.set('loading', false); + }, 500).observes('filter'), + + setLoading: function() { + this.set('loading', true); + }.observes('filter'), + + tables: function() { + if (!this.get('filteredTables')) { + this.set('loading', true); + this.triggerFilter(); + return []; + } + return this.get('filteredTables'); + }.property('transformedSchema', 'filteredTables') +}); diff --git a/assets/javascripts/discourse/routes/admin-plugins-explorer.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-explorer.js.es6 index 0438c9b..9091a82 100644 --- a/assets/javascripts/discourse/routes/admin-plugins-explorer.js.es6 +++ b/assets/javascripts/discourse/routes/admin-plugins-explorer.js.es6 @@ -4,6 +4,17 @@ export default Discourse.Route.extend({ queryParams: { id: { replace: true } }, model() { - return this.store.findAll('query'); + const p1 = this.store.findAll('query'); + const p2 = Discourse.ajax('/admin/plugins/explorer/schema.json', {cache: true}); + return p1.then(function(model) { + return p2.then(function(schema) { + return { content: model, schema: schema }; + }); + }); + }, + + setupController: function(controller, model) { + controller.set('model', model.content); + controller.set('schema', model.schema); } }); diff --git a/assets/javascripts/discourse/templates/admin/plugins-explorer-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-explorer-show.hbs index e8aa3f1..9f67ee0 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-explorer-show.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-explorer-show.hbs @@ -1,27 +1,39 @@ {{#if selectedItem}} -
- {{#if editName}} - {{text-field value=selectedItem.name}} - {{else}} -

{{selectedItem.name}}

- {{d-button action="editName" icon="pencil" class="no-text btn-small"}} - {{/if}} +
+ + {{i18n "explorer.schema.title"}}
+ {{{i18n "explorer.schema.type_help"}}} +
+
+ {{explorer-schema schema=schema}} +
-
- {{#if controller.editName}} - {{textarea value=selectedItem.description}} - {{else}} - {{selectedItem.description}} - {{/if}} +
+
+ {{#if editName}} + {{text-field value=selectedItem.name}} + {{else}} +

{{selectedItem.name}}

+ {{d-button action="editName" icon="pencil" class="no-text btn-small"}} + {{/if}} +
+
+ {{#if controller.editName}} + {{textarea value=selectedItem.description}} + {{else}} + {{selectedItem.description}} + {{/if}} +
+
+ {{textarea value=selectedItem.sql}} +
-
- {{textarea value=selectedItem.sql}} -
-
+
+
{{d-button action="save" label="explorer.save" disabled=saveDisabled}} {{d-button action="download" label="explorer.export" disabled=runDisabled icon="download"}}
-
+
{{#if selectedItem.destroyed}} {{d-button action="recover" class="" icon="undo" label="explorer.recover"}} {{else}} diff --git a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs index 5ef9446..19f619b 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs @@ -26,7 +26,7 @@ {{i18n "explorer.no_queries"}} {{i18n "explorer.no_queries_hook"}} {{else}}
- {{partial "admin/plugins-explorer-show" model=selectedItem}} + {{partial "admin/plugins-explorer-show" model=selectedItem schema=schema}}
diff --git a/assets/javascripts/discourse/templates/components/explorer-schema-onetable.hbs b/assets/javascripts/discourse/templates/components/explorer-schema-onetable.hbs new file mode 100644 index 0000000..04de436 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/explorer-schema-onetable.hbs @@ -0,0 +1,35 @@ +
+ {{#if table.open}} + + {{else}} + + {{/if}} + {{table.name}} +
+
+ {{#if table.open}} + {{#each table.columns as |col|}} +
+ {{#if col.sensitive}} + + + {{col.column_name}} + + {{else}} + + {{col.column_name}} + + {{/if}} + + {{col.data_type}} + + {{#if col.notes}} + + {{col.notes}} + + {{/if}} + +
+ {{/each}} + {{/if}} +
diff --git a/assets/javascripts/discourse/templates/components/explorer-schema.hbs b/assets/javascripts/discourse/templates/components/explorer-schema.hbs new file mode 100644 index 0000000..613a042 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/explorer-schema.hbs @@ -0,0 +1,7 @@ +{{text-field value=filter placeholderKey="explorer.schema.filter"}} +{{conditional-loading-spinner condition=loading}} +
+ {{#each tables as |table|}} + {{explorer-schema-onetable table=table}} + {{/each}} +
diff --git a/assets/stylesheets/explorer.scss b/assets/stylesheets/explorer.scss index a494ac3..9f47f76 100644 --- a/assets/stylesheets/explorer.scss +++ b/assets/stylesheets/explorer.scss @@ -13,20 +13,76 @@ margin: 10px 0; } .sql textarea { - width: calc(100% - 8px); - height: 100px; + width: 500px; + height: 350px; font-family: monospace; border-color: $tertiary; } - .left-buttons { - float: left; - } - .right-buttons { - float: right; - } .clear { clear: both; } } +.schema { + border: 1px solid black; + overflow-y: scroll; + > div { + width: 306px; + } + max-height: 400px; + + margin-bottom: 10px; +} +.schema-title { + display: block; + margin: auto; + text-align: center; +} +.schema input { + padding: 4px; + margin: 3px; + width: calc(100% - 7px - 7px - 1px); +} +.schema .schema-table:first-child { + border-top: 1px solid; +} +.schema-table-name { + background: dark-light-diff($primary, $secondary, 80%, -20%); + border-color: dark-light-diff($primary, $secondary, 60%, -20%); + font-weight: bold; + border-bottom: 1px solid; + padding-left: 5px; +} +.schema-table-cols { + +} +.schema-table-col { + background: $secondary; + border-color: dark-light-diff($primary, $secondary, 60%, -20%); + border-bottom: 1px solid; + padding-left: 10px; +} +.schema-colname { + font-weight: bold; + + display: inline-block; + width: 150px; + word-wrap: break-word; + vertical-align: text-top; + background: dark-light-diff($primary, $secondary, 98%, -20%); + + &.sensitive { + color: $danger; + } +} +.schema-type { + display: inline-block; + max-width: 140px; + vertical-align: text-top; +} +.schema-typenotes { + color: dark-light-diff($primary, $secondary, 50%, -20%); + font-style: italic; +} + .query-params { border: 1px solid dark-light-diff($primary, $secondary, 60%, -20%); input { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a1e373f..412ce4c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -28,6 +28,11 @@ en: import: label: "Import" modal: "Import A Query" + schema: + title: "Database Schema" + filter: "Search..." + sensitive: "The contents of this column may contain particularly sensitive or private information." + type_help: "Types explanation" export: "Export" save: "Save Changes" run: "Run Query" diff --git a/plugin.rb b/plugin.rb index d6d1d22..1a8fee7 100644 --- a/plugin.rb +++ b/plugin.rb @@ -37,7 +37,8 @@ after_initialize do isolate_namespace DataExplorer end - class ValidationError < StandardError; end + class ValidationError < StandardError; + end # Extract :colon-style parameters from the SQL query and replace them with # $1-style parameters. @@ -134,6 +135,97 @@ SQL } end + def self.sensitive_column_names + %w( +#_IP_Addresses +topic_views.ip_address +users.ip_address +users.registration_ip_address +incoming_links.ip_address +topic_link_clicks.ip_address +user_histories.ip_address + +#_Emails +email_tokens.email +users.email +invites.email +user_histories.email +email_logs.to_address +posts.raw_email +badge_posts.raw_email + +#_Secret_Tokens +email_tokens.token +email_logs.reply_key +api_keys.key +site_settings.value + +users.password_hash +users.salt + +#_Authentication_Info +user_open_ids.email +oauth2_user_infos.uid +oauth2_user_infos.email +facebook_user_infos.facebook_user_id +facebook_user_infos.email +twitter_user_infos.twitter_user_id +github_user_infos.github_user_id +single_sign_on_records.external_email +single_sign_on_records.external_id +google_user_infos.google_user_id +google_user_infos.email + ) + end + + def self.schema + # refer user to http://www.postgresql.org/docs/9.3/static/datatype.html + @schema ||= begin + results = ActiveRecord::Base.exec_sql < "query#schema" get 'queries' => "query#index" post 'queries' => "query#create" get 'queries/:id' => "query#show"