Skip to content

Commit

Permalink
index crud actions
Browse files Browse the repository at this point in the history
  • Loading branch information
rathboma committed Jul 21, 2021
1 parent 0bd9d7b commit 44f2b8e
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 73 deletions.
139 changes: 120 additions & 19 deletions apps/studio/src/components/tableinfo/TableIndexes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<div class="table-title">
<h2>Indexes</h2>
</div>
<div class="table-actions">
<!-- <a class="btn btn-flat btn-icon btn-small"><i class="material-icons">add</i> Index</a> -->
<span class="expand"></span>
<div class="actions">
<a @click.prevent="addRow" class="btn btn-primary btn-fab"><i class="material-icons">add</i></a>
</div>

</div>
<div class="table-indexes" ref="tabulator"></div>
</div>
Expand All @@ -19,57 +21,156 @@
<status-bar class="tabulator-footer">
<div class="flex flex-middle flex-right statusbar-actions">
<slot name="footer" />
<x-button v-if="hasEdits" class="btn btn-flat reset" @click.prevent="submitUndo">Reset</x-button>
<x-buttons v-if="hasEdits" class="pending-changes">
<x-button class="btn btn-primary" @click.prevent="submitApply">
<i v-if="error" class="material-icons">error</i>
<span class="badge" v-if="!error"><small>{{editCount}}</small></span>
<span>Apply</span>
</x-button>
<x-button class="btn btn-primary" menu>
<i class="material-icons">arrow_drop_down</i>
<x-menu>
<x-menuitem @click.prevent="submitSql">
Copy to SQL
</x-menuitem>
</x-menu>
</x-button>
</x-buttons>

<slot name="actions" />
</div>
</status-bar>
</div>
</template>
<script>
import Tabulator from 'tabulator-tables'
<script lang="ts">
import Tabulator, { CellComponent, RowComponent } from 'tabulator-tables'
import data_mutators from '../../mixins/data_mutators'
import globals from '../../common/globals'
import { vueFormatter } from '@shared/lib/tabulator/helpers'
import { TabulatorStateWatchers, trashButton, vueEditor, vueFormatter } from '@shared/lib/tabulator/helpers'
import CheckboxFormatterVue from '@shared/components/tabulator/CheckboxFormatter.vue'
import StatusBar from '../common/StatusBar.vue'
export default {
import Vue from 'vue'
import _ from 'lodash'
import NullableInputEditorVue from '@shared/components/tabulator/NullableInputEditor.vue'
import CheckboxEditorVue from '@shared/components/tabulator/CheckboxEditor.vue'
interface State {
tabulator: Tabulator
newRows: RowComponent[]
removedRows: RowComponent[]
}
export default Vue.extend({
components: {
StatusBar,
},
mixins: [data_mutators],
props: ["table", "connection", "tabId", "active", "properties"],
data() {
data(): State {
return {
tabulator: null
tabulator: null,
newRows: [],
removedRows: []
}
},
watch: {
tableData() {
if (!this.tabulator) return
this.tabulator.replaceData(this.tableData)
}
...TabulatorStateWatchers
},
computed: {
editCount() {
return this.newRows.length + this.removedRows.length;
},
hasEdits() {
return this.editCount > 0
},
tableData() {
return this.properties.indexes || []
},
tableColumns() {
const editable = (cell) => this.newRows.includes(cell.getRow())
return [
{title: 'Id', field: 'id', widthGrow: 0.5},
{title:'Name', field: 'name'},
{title: 'Unique', field: 'unique', formatter: vueFormatter(CheckboxFormatterVue), width: 80},
{
title:'Name',
field: 'name',
editable,
editor: vueEditor(NullableInputEditorVue),
formatter: this.cellFormatter
},
{
title: 'Unique',
field: 'unique',
formatter: vueFormatter(CheckboxFormatterVue),
formatterParams: {
editable,
},
width: 80,
editable,
editor: vueEditor(CheckboxEditorVue)
},
{title: 'Primary', field: 'primary', formatter: vueFormatter(CheckboxFormatterVue), width: 85},
{title: 'Columns', field: 'columns'}
{
title: 'Columns',
field: 'columns',
editable,
editor: 'select',
formatter: this.cellFormatter,
editorParams: {
multiselect: true,
values: this.table.columns.map((c) => c.columnName)
}
},
trashButton(this.removeRow)
]
}
},
methods: {
async addRow() {
const tabulator = this.tabulator as Tabulator
const row = await tabulator.addRow({
name: null,
unique: true
})
row.getElement().classList.add('inserted')
this.newRows.push(row)
},
async removeRow(e: any, cell: CellComponent) {
const row = cell.getRow()
if (this.newRows.includes(row)) {
this.newRows = _.without(this.newRows, row)
row.delete()
return
}
this.removedRows = this.removedRows.includes(row) ?
_.without(this.removedRows, row) :
[...this.removedRows, row]
},
clearChanges() {
this.newRows = []
this.removedRows = []
},
submitUndo() {
this.newRows.forEach((r) => r.delete())
this.clearChanges()
},
submitApply() {
},
submitSql() {
}
},
mounted() {
this.tabulator = new Tabulator(this.$refs.tabulator, {
data: this.tableData,
columns: this.tableColumns,
layout: 'fitColumns',
columnMaxInitialWidth: globals.maxColumnWidthTableInfo,
placeholder: "No Indexes"
placeholder: "No Indexes",
resizableColumns: false,
headerSort: false,
})
}
}
})
</script>
52 changes: 3 additions & 49 deletions apps/studio/src/components/tableinfo/TableSchema.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import sqlFormatter from 'sql-formatter'
import _ from 'lodash'
import Vue from 'vue'
// import globals from '../../common/globals'
import { vueEditor, vueFormatter } from '@shared/lib/tabulator/helpers'
import { vueEditor, vueFormatter, trashButton, TabulatorStateWatchers } from '@shared/lib/tabulator/helpers'
import CheckboxFormatterVue from '@shared/components/tabulator/CheckboxFormatter.vue'
import CheckboxEditorVue from '@shared/components/tabulator/CheckboxEditor.vue'
import NullableInputEditorVue from '@shared/components/tabulator/NullableInputEditor.vue'
Expand Down Expand Up @@ -84,44 +84,7 @@ export default Vue.extend({
hasEdits() {
this.tabState.dirty = this.hasEdits
},
active() {
if (!this.tabulator) return;
if (this.active) {
this.tabulator.restoreRedraw()
this.$nextTick(() => {
this.tabulator.redraw(this.forceRedraw)
this.forceRedraw = false
})
} else {
this.tabulator.blockRedraw()
}
},
editedCells(newCells: CellComponent[], oldCells: CellComponent[]) {
const removed = oldCells.filter((c) => !newCells.includes(c))
newCells.forEach((c) => c.getElement().classList.add('edited'))
removed.forEach((c) => c.getElement().classList.remove('edited'))
},
newRows(nuRows: RowComponent[], oldRows: RowComponent[]) {
const removed = oldRows.filter((r) => !nuRows.includes(r))
nuRows.forEach((r) => {
r.getElement().classList?.add('inserted')
})
removed.forEach((r) => {
r.getElement().classList?.remove('inserted')
})
},
removedRows(newRemoved: RowComponent[], oldRemoved: RowComponent[]) {
const removed = oldRemoved.filter((r) => !newRemoved.includes(r))
newRemoved.forEach((r) => r.getElement().classList?.add('deleted'))
removed.forEach((r) => r.getElement().classList?.remove('deleted'))
},
tableData: {
deep: true,
handler() {
if (!this.tabulator) return
this.tabulator.replaceData(this.tableData)
}
}
...TabulatorStateWatchers
},
computed: {
...mapGetters(['dialect']),
Expand Down Expand Up @@ -200,16 +163,7 @@ export default Vue.extend({
width: 70,
cssClass: "read-only never-editable",
},
{
field: 'trash-button',
formatter: (_cell) => `<div class="dynamic-action" />`,
width: 36,
minWidth: 36,
hozAlign: 'center',
cellClick: this.removeRow,
resizable: false,
cssClass: "remove-btn read-only",
}
trashButton(this.removeRow)
]
return result.map((col) => {
const editable = _.isFunction(col.editable) ? col.editable({ getRow: () => ({})}) : col.editable
Expand Down
5 changes: 4 additions & 1 deletion apps/studio/src/lib/db/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import clients from './clients';
import createLogger from '../logger';
import { SSHConnection } from 'node-ssh-forward';
import { SupportedFeatures, FilterOptions, TableOrView, Routine, TableColumn, SchemaFilterOptions, DatabaseFilterOptions, TableChanges, TableUpdateResult, OrderBy, TableFilter, TableResult, StreamResults, CancelableQuery, ExtendedTableColumn, PrimaryKeyColumn, TableProperties, TableIndex, TableTrigger } from './models';
import { AlterTableSpec } from '@shared/lib/dialects/models';
import { AlterTableSpec, CreateIndexSpec, DropIndexSpec } from '@shared/lib/dialects/models';

const logger = createLogger('db');

Expand All @@ -29,6 +29,9 @@ export interface DatabaseClient {
alterTableSql: (change: AlterTableSpec) => Promise<string>,
alterTable: (change: AlterTableSpec) => Promise<void>,

createIndex: (specs: CreateIndexSpec[]) => Promise<void>,
dropIndex: (specs: DropIndexSpec[]) => Promise<void>,

getQuerySelectTop: (table: string, limit: number, schema?: string) => void,
getTableProperties: (table: string, schema?: string) => Promise<TableProperties | null>,
getTableCreateScript: (table: string, schema?: string) => Promise<string>,
Expand Down
53 changes: 52 additions & 1 deletion apps/studio/src/lib/db/clients/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import globals from '../../../common/globals';
import { HasPool, VersionInfo, HasConnection, Conn } from './postgresql/types'
import { PsqlCursor } from './postgresql/PsqlCursor';
import { PostgresqlChangeBuilder } from '@shared/lib/sql/change_builder/PostgresqlChangeBuilder';
import { AlterTableSpec } from '@shared/lib/dialects/models';
import { AlterTableSpec, CreateIndexSpec, DropIndexSpec } from '@shared/lib/dialects/models';
import { RedshiftChangeBuilder } from '@shared/lib/sql/change_builder/RedshiftChangeBuilder';
import { PostgresData } from '@shared/lib/dialects/postgresql';


const base64 = require('base64-url');
const PD = PostgresData

function isConnection(x: any): x is HasConnection {
return x.connection !== undefined
Expand All @@ -38,6 +40,11 @@ const pgErrors = {

let dataTypes: any = {}

function tableName(table: string, schema?: string): string{
return schema ? `${PD.wrapIdentifier(schema)}.${PD.wrapIdentifier(table)}` : PD.wrapIdentifier(table);
}


/**
* Do not convert DATE types to JS date.
* It ignores of applying a wrong timezone to the date.
Expand Down Expand Up @@ -178,6 +185,10 @@ export default async function (server: any, database: any): Promise<DatabaseClie
getTableProperties: (table, schema = defaultSchema) => getTableProperties(conn, table, schema),
alterTableSql: (change: AlterTableSpec) => alterTableSql(conn, change),
alterTable: (change: AlterTableSpec) => alterTable(conn, change),

createIndex: (specs: CreateIndexSpec[]) => createIndex(conn, specs),
dropIndex: (specs) => dropIndex(conn, specs),

setTableDescription: (table: string, description: string, schema = defaultSchema) => setTableDescription(conn, table, description, schema)
};
}
Expand Down Expand Up @@ -907,6 +918,30 @@ export async function alterTable(_conn: HasPool, change: AlterTableSpec) {
})
}

function createIndexSql(spec: CreateIndexSpec): string {
const unique = spec.unique ? 'UNIQUE' : ''
const name = spec.name ? PD.wrapIdentifier(spec.name) : ''
const table = tableName(spec.table, spec.schema)
const columns = spec.columns.map((c) => {
return PD.wrapIdentifier(c)
})
return `
CREATE ${unique} INDEX ${name} on ${table}(${columns})
`
}

export async function createIndex(conn: HasPool, specs: CreateIndexSpec[]) {
const queries = specs.map((spec) => createIndexSql(spec))
await executeWithTransaction(conn, { query: queries.join(";") })
}

export async function dropIndex(conn: HasPool, specs: DropIndexSpec[]) {
if (!specs.length) return
const names = specs.map((spec) => PD.wrapIdentifier(spec.name)).join(",")
const query = `DROP INDEX ${names}`
await executeWithTransaction(conn, { query })
}

export async function setTableDescription(conn: HasPool, table: string, description: string, schema: string): Promise<string> {
const identifier = wrapTable(table, schema)
const comment = escapeString(description)
Expand Down Expand Up @@ -1284,6 +1319,22 @@ async function driverExecuteSingle(conn: Conn | HasConnection, queryArgs: Postgr
return (await driverExecuteQuery(conn, queryArgs))[0]
}

async function executeWithTransaction(conn: Conn | HasConnection, queryArgs: PostgresQueryArgs): Promise<QueryResult[]> {
const fullQuery = [
'BEGIN', queryArgs.query, 'COMMIT'
].join(";")
return await runWithConnection(conn, async (connection) => {
const cli = { connection }
try {
return await driverExecuteQuery(cli, { ...queryArgs, query: fullQuery})
} catch (ex) {
log.error("Query Exception", ex)
await driverExecuteSingle(cli, { query: "ROLLBACK" })
throw ex;
}
})
}

function driverExecuteQuery(conn: Conn | HasConnection, queryArgs: PostgresQueryArgs): Promise<QueryResult[]> {

const runQuery = (connection: pg.PoolClient): Promise<QueryResult[]> => {
Expand Down
12 changes: 11 additions & 1 deletion apps/studio/src/mixins/data_mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@ export default {

methods: {
cellFormatter(cell: Tabulator.CellComponent) {

const nullValue = '<span class="null-value">(NULL)</span>'

if (_.isNil(cell.getValue())) {
return '<span class="null-value">(NULL)</span>'
return nullValue
}
if (_.isString(cell.getValue()) && _.isEmpty(cell.getValue())) {
return '<span class="null-value">(EMPTY)</span>'
}

if (_.isArray(cell.getValue()) && cell.getValue().length === 0) {
return nullValue
}

let cellValue = cell.getValue().toString();
if (_.isArray(cell.getValue())) {
cellValue = cell.getValue().map((v) => v.toString()).join(", ")
}
cellValue = cellValue.replace(/\n/g, ' ↩ ');
cellValue = sanitizeHtml(cellValue);
// removing the <pre> will break selection / copy paste, see ResultTable
Expand Down
Loading

0 comments on commit 44f2b8e

Please sign in to comment.