mirror of https://github.com/Fabio286/antares.git
refactor: improvements to blob editor and code cleanup
This commit is contained in:
parent
712fe9f00d
commit
4fd72ec9e7
|
@ -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'];
|
|
@ -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);
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
export function uidGen () {
|
||||
return Math.random().toString(36).substr(2, 9).toUpperCase();
|
||||
};
|
|
@ -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':
|
||||
|
|
|
@ -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':
|
||||
let reload = false;
|
||||
|
||||
if (NUMBER.includes(params.type))
|
||||
escapedParam = params.content;
|
||||
break;
|
||||
case 'char':
|
||||
case 'varchar':
|
||||
case 'text':
|
||||
case 'mediumtext':
|
||||
case 'longtext':
|
||||
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
|
||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||
break;
|
||||
case 'blob':
|
||||
case 'mediumblob':
|
||||
case 'longblob': {
|
||||
else if (BLOB.includes(params.type)) {
|
||||
if (params.content) {
|
||||
const fileBlob = fs.readFileSync(params.content);
|
||||
escapedParam = `0x${fileBlob.toString('hex')}`;
|
||||
reload = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
else
|
||||
escapedParam = '""';
|
||||
}
|
||||
else
|
||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||
break;
|
||||
}
|
||||
return connection
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -143,6 +143,9 @@ export default {
|
|||
}
|
||||
|
||||
this.isQuering = false;
|
||||
},
|
||||
reloadTable () {
|
||||
this.runQuery();// TODO: run last executed query
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
<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"
|
||||
class="img-responsive p-centered bg-checkered"
|
||||
>
|
||||
<div v-if="contentInfo.size" class="editor-buttons mt-2">
|
||||
<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>
|
||||
</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,40 +136,32 @@ export default {
|
|||
typeFormat (val, type, precision) {
|
||||
if (!val) return val;
|
||||
|
||||
switch (type) {
|
||||
case 'char':
|
||||
case 'varchar':
|
||||
case 'text':
|
||||
case 'mediumtext':
|
||||
return val;
|
||||
case 'date': {
|
||||
if (DATE.includes(type))
|
||||
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
|
||||
}
|
||||
case 'datetime':
|
||||
case 'timestamp': {
|
||||
|
||||
if (DATETIME.includes(type)) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < precision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
|
||||
return moment(val).isValid() ? moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`) : val;
|
||||
}
|
||||
case 'blob':
|
||||
case 'mediumblob':
|
||||
case 'longblob': {
|
||||
|
||||
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)})`;
|
||||
}
|
||||
case 'bit': {
|
||||
|
||||
if (BIT.includes(type)) {
|
||||
const hex = Buffer.from(val).toString('hex');
|
||||
return hexToBinary(hex);
|
||||
}
|
||||
default:
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
|
@ -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':
|
||||
if ([...TEXT, ...LONG_TEXT].includes(this.type))
|
||||
return { type: 'text', mask: false };
|
||||
case 'int':
|
||||
case 'tinyint':
|
||||
case 'smallint':
|
||||
case 'mediumint':
|
||||
case 'bigint':
|
||||
|
||||
if (NUMBER.includes(this.type))
|
||||
return { type: 'number', mask: false };
|
||||
case 'date':
|
||||
|
||||
if (TIME.includes(this.type))
|
||||
return { type: 'number', mask: false };
|
||||
|
||||
if (DATE.includes(this.type))
|
||||
return { type: 'text', mask: '####-##-##' };
|
||||
case 'datetime':
|
||||
case 'timestamp': {
|
||||
|
||||
if (DATETIME.includes(this.type)) {
|
||||
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':
|
||||
|
||||
if (BLOB.includes(this.type))
|
||||
return { type: 'file', mask: false };
|
||||
default:
|
||||
return 'hidden';
|
||||
}
|
||||
|
||||
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,21 +228,17 @@ export default {
|
|||
return bufferToBase64(val);
|
||||
},
|
||||
editON () {
|
||||
switch (this.type) {
|
||||
// Large text editor
|
||||
case 'text':
|
||||
case 'mediumtext':
|
||||
case 'longtext':
|
||||
if (LONG_TEXT.includes(this.type)) {
|
||||
this.isTextareaEditor = true;
|
||||
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||
break;
|
||||
// File fields editor
|
||||
case 'blob':
|
||||
case 'mediumblob':
|
||||
case 'longblob':
|
||||
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);
|
||||
|
@ -253,10 +252,10 @@ export default {
|
|||
};
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
// Inline editable fields
|
||||
default:
|
||||
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||
this.$nextTick(() => { // Focus on input
|
||||
this.$refs.cell.blur();
|
||||
|
@ -264,8 +263,6 @@ export default {
|
|||
this.$nextTick(() => this.$refs.editField.focus());
|
||||
});
|
||||
this.isInlineEditor = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
editOFF () {
|
||||
this.isInlineEditor = false;
|
||||
|
@ -275,9 +272,15 @@ export default {
|
|||
content = this.localContent;
|
||||
}
|
||||
else { // Handle file upload
|
||||
if (this.willBeDeleted) {
|
||||
content = '';
|
||||
this.willBeDeleted = false;
|
||||
}
|
||||
else {
|
||||
if (!this.fileToUpload) return;
|
||||
content = this.fileToUpload.file.path;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('updateField', {
|
||||
field: this.field,
|
||||
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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')
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use strict';
|
||||
import { uidGen } from 'common/libs/utilities';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue