refactor: improvements to blob editor and code cleanup

This commit is contained in:
Fabio 2020-08-04 17:54:19 +02:00
parent 712fe9f00d
commit 4fd72ec9e7
18 changed files with 255 additions and 174 deletions

12
src/common/fieldTypes.js Normal file
View File

@ -0,0 +1,12 @@
export const TEXT = ['char', 'varchar'];
export const LONG_TEXT = ['text', 'mediumtext', 'longtext'];
export const NUMBER = ['int', 'tinyint', 'smallint', 'mediumint', 'bigint'];
export const DATE = ['date'];
export const TIME = ['time'];
export const DATETIME = ['datetime', 'timestamp'];
export const BLOB = ['blob', 'mediumblob', 'longblob'];
export const BIT = ['bit'];

View File

@ -0,0 +1,7 @@
'use strict';
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}

View File

@ -0,0 +1,12 @@
'use strict';
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

View File

@ -1,7 +1,4 @@
export function uidGen () {
return Math.random().toString(36).substr(2, 9).toUpperCase();
};
'use strict';
export function mimeFromHex (hex) {
switch (hex.substring(0, 4)) { // 2 bytes
case '424D':
@ -39,28 +36,11 @@ export function mimeFromHex (hex) {
return { ext: 'bpg', mime: 'image/bpg' };
case '4D4D002A':
return { ext: 'tif', mime: 'image/tiff' };
case '00000100':
return { ext: 'ico', mime: 'image/x-icon' };
default:
return { ext: '', mime: 'unknown ' + hex };
}
}
}
};
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch);
}).join('');
return btoa(binstr);
}

View File

@ -0,0 +1,4 @@
'use strict';
export function uidGen () {
return Math.random().toString(36).substr(2, 9).toUpperCase();
};

View File

@ -248,7 +248,7 @@ export class AntaresConnector {
* @memberof AntaresConnector
*/
async raw (sql) {
if (process.env.NODE_ENV === 'development') this._logger(sql);
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
switch (this._client) { // TODO: uniform fields with every client type, needed table name and fields array
case 'maria':

View File

@ -1,5 +1,6 @@
'use strict';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import fs from 'fs';
export default class {
@ -14,38 +15,32 @@ export default class {
static async updateTableCell (connection, params) {
let escapedParam;
switch (params.type) {
case 'int':
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'bigint':
escapedParam = params.content;
break;
case 'char':
case 'varchar':
case 'text':
case 'mediumtext':
case 'longtext':
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
case 'blob':
case 'mediumblob':
case 'longblob': {
let reload = false;
if (NUMBER.includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
reload = true;
}
break;
default:
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
else
escapedParam = '""';
}
return connection
else
escapedParam = `"${sqlEscaper(params.content)}"`;
await connection
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${params.id}` })
.run();
return { reload };
}
static async deleteTableRows (connection, params) {

View File

@ -29,13 +29,13 @@
class="btn btn-primary mr-2"
@click="confirmModal"
>
{{ $t('word.confirm') }}
{{ confirmText || $t('word.confirm') }}
</button>
<button
class="btn btn-link"
@click="hideModal"
>
{{ $t('word.cancel') }}
{{ cancelText || $t('word.cancel') }}
</button>
</div>
</div>
@ -48,8 +48,11 @@ export default {
props: {
size: {
type: String,
default: 'small' // small, medium, large
}
validator: prop => ['small', 'medium', 'large'].includes(prop),
default: 'small'
},
confirmText: String,
cancelText: String
},
computed: {
hasHeader () {

View File

@ -148,7 +148,7 @@
<script>
import { mapActions } from 'vuex';
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/utilities';
import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';

View File

@ -143,6 +143,9 @@ export default {
}
this.isQuering = false;
},
reloadTable () {
this.runQuery();// TODO: run last executed query
}
}
};

View File

@ -66,7 +66,7 @@
</template>
<script>
import { uidGen } from 'common/libs/utilities';
import { uidGen } from 'common/libs/uidGen';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableCell from '@/components/WorkspaceQueryTableCell';
import TableContext from '@/components/WorkspaceQueryTableContext';

View File

@ -35,6 +35,7 @@
</template>
<ConfirmModal
v-if="isTextareaEditor"
:confirm-text="$t('word.update')"
size="medium"
@confirm="editOFF"
@hide="hideEditorModal"
@ -59,6 +60,7 @@
</ConfirmModal>
<ConfirmModal
v-if="isBlobEditor"
:confirm-text="$t('word.update')"
@confirm="editOFF"
@hide="hideEditorModal"
>
@ -67,19 +69,28 @@
</template>
<div :slot="'body'">
<div class="mb-2">
<div>
<img
v-if="isImage"
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(localContent)}`"
class="img-responsive p-centered"
>
<div v-if="contentInfo.size" class="editor-buttons mt-2">
<button class="btn btn-link btn-sm" @click="downloadFile">
<span>{{ $t('word.download') }}</span>
<i class="material-icons ml-1">file_download</i>
</button>
<transition name="jump-down">
<div v-if="contentInfo.size">
<img
v-if="isImage"
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(localContent)}`"
class="img-responsive p-centered bg-checkered"
>
<div v-else class="text-center">
<i class="material-icons md-36">insert_drive_file</i>
</div>
<div class="editor-buttons mt-2">
<button class="btn btn-link btn-sm" @click="downloadFile">
<span>{{ $t('word.download') }}</span>
<i class="material-icons ml-1">file_download</i>
</button>
<button class="btn btn-link btn-sm" @click="prepareToDelete">
<span>{{ $t('word.delete') }}</span>
<i class="material-icons ml-1">delete_forever</i>
</button>
</div>
</div>
</div>
</transition>
<div class="editor-field-info">
<div>
<b>{{ $t('word.size') }}</b>: {{ localContent.length | formatBytes }}<br>
@ -103,8 +114,11 @@
<script>
import moment from 'moment';
import { mimeFromHex, formatBytes, bufferToBase64 } from 'common/libs/utilities';
import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
@ -122,39 +136,31 @@ export default {
typeFormat (val, type, precision) {
if (!val) return val;
switch (type) {
case 'char':
case 'varchar':
case 'text':
case 'mediumtext':
return val;
case 'date': {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
}
case 'datetime':
case 'timestamp': {
let datePrecision = '';
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
if (DATE.includes(type))
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
return moment(val).isValid() ? moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`) : val;
}
case 'blob':
case 'mediumblob':
case 'longblob': {
const buff = Buffer.from(val);
if (!buff.length) return '';
if (DATETIME.includes(type)) {
let datePrecision = '';
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
}
case 'bit': {
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
}
default:
return val;
return moment(val).isValid() ? moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`) : val;
}
if (BLOB.includes(type)) {
const buff = Buffer.from(val);
if (!buff.length) return '';
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
}
if (BIT.includes(type)) {
const hex = Buffer.from(val).toString('hex');
return hexToBinary(hex);
}
return val;
}
},
directives: {
@ -171,6 +177,7 @@ export default {
isInlineEditor: false,
isTextareaEditor: false,
isBlobEditor: false,
willBeDeleted: false,
localContent: null,
contentInfo: {
ext: '',
@ -182,39 +189,35 @@ export default {
},
computed: {
inputProps () {
switch (this.type) {
case 'char':
case 'varchar':
case 'text':
case 'mediumtext':
case 'longtext':
return { type: 'text', mask: false };
case 'int':
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'bigint':
return { type: 'number', mask: false };
case 'date':
return { type: 'text', mask: '####-##-##' };
case 'datetime':
case 'timestamp': {
let datetimeMask = '####-##-## ##:##:##';
for (let i = 0; i < this.precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
case 'blob':
case 'mediumblob':
case 'longblob':
case 'bit':
return { type: 'file', mask: false };
default:
return 'hidden';
if ([...TEXT, ...LONG_TEXT].includes(this.type))
return { type: 'text', mask: false };
if (NUMBER.includes(this.type))
return { type: 'number', mask: false };
if (TIME.includes(this.type))
return { type: 'number', mask: false };
if (DATE.includes(this.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(this.type)) {
let datetimeMask = '####-##-## ##:##:##';
for (let i = 0; i < this.precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(this.type))
return { type: 'file', mask: false };
if (BIT.includes(this.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
isImage () {
return ['gif', 'jpg', 'png'].includes(this.contentInfo.ext);
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
}
},
methods: {
@ -225,47 +228,41 @@ export default {
return bufferToBase64(val);
},
editON () {
switch (this.type) {
// Large text editor
case 'text':
case 'mediumtext':
case 'longtext':
this.isTextareaEditor = true;
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
break;
// File fields editor
case 'blob':
case 'mediumblob':
case 'longblob':
this.isBlobEditor = true;
this.localContent = this.content ? this.content : '';
this.fileToUpload = null;
if (this.content !== null) {
const buff = Buffer.from(this.localContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
const { ext, mime } = mimeFromHex(hex);
this.contentInfo = {
ext,
mime,
size: this.localContent.length
};
}
}
break;
// Inline editable fields
default:
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
this.$nextTick(() => { // Focus on input
this.$refs.cell.blur();
this.$nextTick(() => this.$refs.editField.focus());
});
this.isInlineEditor = true;
break;
if (LONG_TEXT.includes(this.type)) {
this.isTextareaEditor = true;
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
return;
}
if (BLOB.includes(this.type)) {
this.isBlobEditor = true;
this.localContent = this.content ? this.content : '';
this.fileToUpload = null;
this.willBeDeleted = false;
if (this.content !== null) {
const buff = Buffer.from(this.localContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
const { ext, mime } = mimeFromHex(hex);
this.contentInfo = {
ext,
mime,
size: this.localContent.length
};
}
}
return;
}
// Inline editable fields
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
this.$nextTick(() => { // Focus on input
this.$refs.cell.blur();
this.$nextTick(() => this.$refs.editField.focus());
});
this.isInlineEditor = true;
},
editOFF () {
this.isInlineEditor = false;
@ -275,8 +272,14 @@ export default {
content = this.localContent;
}
else { // Handle file upload
if (!this.fileToUpload) return;
content = this.fileToUpload.file.path;
if (this.willBeDeleted) {
content = '';
this.willBeDeleted = false;
}
else {
if (!this.fileToUpload) return;
content = this.fileToUpload.file.path;
}
}
this.$emit('updateField', {
@ -304,6 +307,16 @@ export default {
if (!files.length) return;
this.fileToUpload = { name: files[0].name, file: files[0] };
this.willBeDeleted = false;
},
prepareToDelete () {
this.localContent = '';
this.contentInfo = {
ext: '',
mime: '',
size: null
};
this.willBeDeleted = true;
}
}
};
@ -338,7 +351,7 @@ export default {
.editor-buttons {
display: flex;
justify-content: center;
justify-content: space-evenly;
.btn {
display: flex;

View File

@ -6,7 +6,7 @@
<button
class="btn btn-link btn-sm"
:class="{'loading':isQuering}"
@click="getTableData"
@click="reloadTable"
>
<span>{{ $t('word.refresh') }}</span>
<i class="material-icons ml-1">refresh</i>
@ -140,6 +140,9 @@ export default {
}
this.isQuering = false;
},
reloadTable () {
this.getTableData();
}
}
};

View File

@ -3,6 +3,8 @@ import Tables from '@/ipc-api/Tables';
export default {
methods: {
async updateField (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
@ -12,16 +14,24 @@ export default {
try {
const { status, response } = await Tables.updateTableCell(params);
if (status === 'success')
this.$refs.queryTable.applyUpdate(payload);
if (status === 'success') {
if (response.reload)// Needed for blob fields
this.reloadTable();
else
this.$refs.queryTable.applyUpdate(payload);
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
async deleteSelected (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
@ -42,6 +52,8 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
}
}
};

View File

@ -11,3 +11,31 @@
transform: translateX(10px);
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.jump-down-enter-active {
animation: jump-down-in 0.2s;
}
.jump-down-leave-active {
animation: jump-down-in 0.2s reverse;
}
@keyframes jump-down-in {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}

View File

@ -36,6 +36,15 @@ body {
cursor: help;
}
.bg-checkered {
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal;
background-size: 2em 2em;
}
// Scrollbars
::-webkit-scrollbar {
width: 10px;

View File

@ -1,5 +1,5 @@
'use strict';
import { uidGen } from 'common/libs/utilities';
import { uidGen } from 'common/libs/uidGen';
export default {
namespaced: true,

View File

@ -1,6 +1,6 @@
'use strict';
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/utilities';
import { uidGen } from 'common/libs/uidGen';
function remapStructure (structure) {
const databases = structure.map(table => table.TABLE_SCHEMA)