mirror of
				https://github.com/Fabio286/antares.git
				synced 2025-06-05 21:59:22 +02:00 
			
		
		
		
	Improvements to query builder
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,3 +5,4 @@ thumbs.db | ||||
| .idea/ | ||||
| .vscode | ||||
| TODO.md | ||||
| *.txt | ||||
							
								
								
									
										30
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1164,9 +1164,9 @@ | ||||
|       "integrity": "sha512-4mXKoDptrXAwZErQHrLzpe0FN/0Wmf5JRniSVIdwUrtDf9wnmEV1teCNLBo/TwuXhkK/bVegoEn/wmb+x0AuPg==" | ||||
|     }, | ||||
|     "@types/readable-stream": { | ||||
|       "version": "2.3.5", | ||||
|       "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.5.tgz", | ||||
|       "integrity": "sha512-Mq2eLkGYamlcolW603FY2ROBvcl90jPF+3jLkjpBV6qS+2aVeJqlgRG0TVAa1oWbmPdb5yOWlOPVvQle76nUNw==", | ||||
|       "version": "2.3.7", | ||||
|       "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.7.tgz", | ||||
|       "integrity": "sha512-oAbOdOKhx5nu1+pBPiwEHh6gqA3ojgwwNwNIjNd67TqwEjvVePqe21mnYc6RrnGZ8aVPFBe6sci3cU2pKKwuug==", | ||||
|       "requires": { | ||||
|         "@types/node": "*", | ||||
|         "safe-buffer": "*" | ||||
| @@ -1522,9 +1522,9 @@ | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/node": { | ||||
|           "version": "8.10.60", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.60.tgz", | ||||
|           "integrity": "sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg==" | ||||
|           "version": "8.10.61", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.61.tgz", | ||||
|           "integrity": "sha512-l+zSbvT8TPRaCxL1l9cwHCb0tSqGAGcjPJFItGGYat5oCTiq1uQQKYg5m7AF1mgnEBzFXGLJ2LRmNjtreRX76Q==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @@ -7715,9 +7715,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "jsbi": { | ||||
|       "version": "3.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.2.tgz", | ||||
|       "integrity": "sha512-5nDXo1X9QVaXK/Cpb5VECV9ss1QPbjUuk1qSruHB1PK/g39Sd414K4nci99ElFDZv0vzxDEnKn3o49/Tn9Yagw==" | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz", | ||||
|       "integrity": "sha512-nBJqA0C6Qns+ZxurbEoIR56wyjiUszpNy70FHvxO5ervMoCbZVE3z3kxr5nKGhlxr/9MhKTSUBs7cAwwuf3g9w==" | ||||
|     }, | ||||
|     "jsbn": { | ||||
|       "version": "0.1.1", | ||||
| @@ -8538,13 +8538,6 @@ | ||||
|         "debug": "^4", | ||||
|         "tarn": "^1.1.5", | ||||
|         "tedious": "^6.6.2" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "tarn": { | ||||
|           "version": "1.1.5", | ||||
|           "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.5.tgz", | ||||
|           "integrity": "sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "multicast-dns": { | ||||
| @@ -11973,6 +11966,11 @@ | ||||
|         "inherits": "2" | ||||
|       } | ||||
|     }, | ||||
|     "tarn": { | ||||
|       "version": "1.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.5.tgz", | ||||
|       "integrity": "sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==" | ||||
|     }, | ||||
|     "tedious": { | ||||
|       "version": "6.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/tedious/-/tedious-6.7.0.tgz", | ||||
|   | ||||
| @@ -4,9 +4,7 @@ import { AntaresConnector } from '../libs/AntaresConnector'; | ||||
| import InformationSchema from '../models/InformationSchema'; | ||||
| import Generic from '../models/Generic'; | ||||
|  | ||||
| const connections = {}; | ||||
|  | ||||
| export default () => { | ||||
| export default (connections) => { | ||||
|    ipcMain.handle('testConnection', async (event, conn) => { | ||||
|       const Connection = new AntaresConnector({ | ||||
|          client: conn.client, | ||||
| @@ -46,9 +44,9 @@ export default () => { | ||||
|          poolSize: 3 | ||||
|       }); | ||||
|  | ||||
|       Connection.connect(); | ||||
|  | ||||
|       try { | ||||
|          await Connection.connect(); | ||||
|  | ||||
|          const { rows: structure } = await InformationSchema.getStructure(Connection); | ||||
|          connections[conn.uid] = Connection; | ||||
|          return { status: 'success', response: structure }; | ||||
| @@ -73,10 +71,10 @@ export default () => { | ||||
|       } | ||||
|    }); | ||||
|  | ||||
|    ipcMain.handle('rawQuery', async (event, { uid, query, database }) => { | ||||
|    ipcMain.handle('rawQuery', async (event, { uid, query, schema }) => { | ||||
|       if (!query) return; | ||||
|       try { | ||||
|          const result = await Generic.raw(connections[uid], query, database); | ||||
|          const result = await Generic.raw(connections[uid], query, schema); | ||||
|          return { status: 'success', response: result }; | ||||
|       } | ||||
|       catch (err) { | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| import connection from './connection'; | ||||
| import structure from './structure'; | ||||
|  | ||||
| const connections = {}; | ||||
|  | ||||
| export default () => { | ||||
|    connection(); | ||||
|    connection(connections); | ||||
|    structure(connections); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/main/ipc-handlers/structure.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/main/ipc-handlers/structure.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { ipcMain } from 'electron'; | ||||
| import InformationSchema from '../models/InformationSchema'; | ||||
| import Generic from '../models/Generic'; | ||||
|  | ||||
| export default (connections) => { | ||||
|    ipcMain.handle('getTableColumns', async (event, { uid, schema, table }) => { | ||||
|       try { | ||||
|          const result = await InformationSchema.getTableColumns(connections[uid], schema, table); | ||||
|          return { status: 'success', response: result }; | ||||
|       } | ||||
|       catch (err) { | ||||
|          return { status: 'error', response: err.toString() }; | ||||
|       } | ||||
|    }); | ||||
|  | ||||
|    ipcMain.handle('getTableData', async (event, { uid, schema, table }) => { | ||||
|       try { | ||||
|          const result = await Generic.getTableData(connections[uid], schema, table); | ||||
|          return { status: 'success', response: result }; | ||||
|       } | ||||
|       catch (err) { | ||||
|          return { status: 'error', response: err.toString() }; | ||||
|       } | ||||
|    }); | ||||
| }; | ||||
| @@ -1,5 +1,6 @@ | ||||
| 'use strict'; | ||||
| import mysql from 'mysql2'; | ||||
| import mssql from 'mssql'; | ||||
|  | ||||
| /** | ||||
|  * As Simple As Possible Query Builder | ||||
| @@ -26,7 +27,7 @@ export class AntaresConnector { | ||||
|          where: [], | ||||
|          groupBy: [], | ||||
|          orderBy: [], | ||||
|          limit: '', | ||||
|          limit: [], | ||||
|          join: [], | ||||
|          update: [], | ||||
|          insert: [], | ||||
| @@ -62,7 +63,7 @@ export class AntaresConnector { | ||||
|    /** | ||||
|     * @memberof AntaresConnector | ||||
|     */ | ||||
|    connect () { | ||||
|    async connect () { | ||||
|       switch (this._client) { | ||||
|          case 'maria': | ||||
|          case 'mysql': | ||||
| @@ -75,7 +76,15 @@ export class AntaresConnector { | ||||
|                this._connection = pool.promise(); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|          case 'mssql': { | ||||
|             const mssqlParams = { | ||||
|                user: this._params.user, | ||||
|                password: this._params.password, | ||||
|                server: this._params.host | ||||
|             }; | ||||
|             this._connection = await mssql.connect(mssqlParams); | ||||
|          } | ||||
|             break; | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
| @@ -111,28 +120,81 @@ export class AntaresConnector { | ||||
|       return this; | ||||
|    } | ||||
|  | ||||
|    getQueryString () { | ||||
|       const selectArray = this._query.select.reduce(this._reducer, []); | ||||
|       const selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')}` : 'SELECT *'; | ||||
|       const fromRaw = this._query.from ? `FROM ${this._query.schema ? `\`${this._query.schema}\`.` : ''} \`${this._query.from}\`` : ''; | ||||
|       const whereArray = this._query.where.reduce(this._reducer, []); | ||||
|       const whereRaw = whereArray.length ? `WHERE ${whereArray.join(', AND ')}` : ''; | ||||
|       const groupByArray = this._query.groupBy.reduce(this._reducer, []); | ||||
|       const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')}` : ''; | ||||
|       const orderByArray = this._query.orderBy.reduce(this._reducer, []); | ||||
|       const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')}` : ''; | ||||
|  | ||||
|       return `${selectRaw} ${fromRaw} ${whereRaw} ${groupByRaw} ${orderByRaw}`; | ||||
|    limit (...args) { | ||||
|       this._query.limit = args; | ||||
|       return this; | ||||
|    } | ||||
|  | ||||
|    run () { | ||||
|       const rawQuery = this.getQueryString(); | ||||
|    /** | ||||
|     * @returns {string} SQL string | ||||
|     * @memberof AntaresConnector | ||||
|     */ | ||||
|    getSQL () { | ||||
|       const selectArray = this._query.select.reduce(this._reducer, []); | ||||
|       let selectRaw; | ||||
|       switch (this._client) { | ||||
|          case 'maria': | ||||
|          case 'mysql': | ||||
|             selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; | ||||
|             break; | ||||
|          case 'mssql': { | ||||
|             const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : ''; | ||||
|             selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * '; | ||||
|          } | ||||
|             break; | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
|  | ||||
|       let fromRaw; | ||||
|       switch (this._client) { | ||||
|          case 'maria': | ||||
|          case 'mysql': | ||||
|             fromRaw = this._query.from ? `FROM ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : ''; | ||||
|             break; | ||||
|          case 'mssql': | ||||
|             fromRaw = this._query.from ? `FROM ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : ''; | ||||
|             break; | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
|  | ||||
|       const whereArray = this._query.where.reduce(this._reducer, []); | ||||
|       const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; | ||||
|       const groupByArray = this._query.groupBy.reduce(this._reducer, []); | ||||
|       const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; | ||||
|       const orderByArray = this._query.orderBy.reduce(this._reducer, []); | ||||
|       const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; | ||||
|  | ||||
|       let limitRaw; | ||||
|       switch (this._client) { | ||||
|          case 'maria': | ||||
|          case 'mysql': | ||||
|             limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; | ||||
|             break; | ||||
|          case 'mssql': | ||||
|             limitRaw = ''; | ||||
|             break; | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
|  | ||||
|       return `${selectRaw}${fromRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}`; | ||||
|    } | ||||
|  | ||||
|    /** | ||||
|     * @returns {Promise} | ||||
|     * @memberof AntaresConnector | ||||
|     */ | ||||
|    async run () { | ||||
|       const rawQuery = this.getSQL(); | ||||
|       if (process.env.NODE_ENV === 'development') console.log(rawQuery); | ||||
|       this._resetQuery(); | ||||
|       return this.raw(rawQuery); | ||||
|    } | ||||
|  | ||||
|    /** | ||||
|     * @param {*} sql raw SQL query | ||||
|     * @param {string} sql raw SQL query | ||||
|     * @returns {Promise} | ||||
|     * @memberof AntaresConnector | ||||
|     */ | ||||
| @@ -143,6 +205,10 @@ export class AntaresConnector { | ||||
|             const [rows, fields] = await this._connection.query(sql); | ||||
|             return { rows, fields }; | ||||
|          } | ||||
|          case 'mssql': { | ||||
|             const results = await this._connection.request().query(sql); | ||||
|             return { rows: results.recordsets[0] }; | ||||
|          } | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
| @@ -154,10 +220,12 @@ export class AntaresConnector { | ||||
|    destroy () { | ||||
|       switch (this._client) { | ||||
|          case 'maria': | ||||
|          case 'mysql': { | ||||
|          case 'mysql': | ||||
|             this._connection.end(); | ||||
|             break; | ||||
|          } | ||||
|          case 'mssql': | ||||
|             this._connection.close(); | ||||
|             break; | ||||
|          default: | ||||
|             break; | ||||
|       } | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| 'use strict'; | ||||
| export default class { | ||||
|    static async raw (connection, query, database) { | ||||
|       if (database) await connection.raw(`USE \`${database}\``); | ||||
|    static async raw (connection, query, schema) { | ||||
|       if (schema) await connection.raw(`USE \`${schema}\``); | ||||
|       return connection.raw(query); | ||||
|    } | ||||
|  | ||||
|    static async getTableData (connection, schema, table) { | ||||
|       return connection | ||||
|          .select('*') | ||||
|          .schema(schema) | ||||
|          .from(table) | ||||
|          .limit(1000) | ||||
|          .run(); | ||||
|    } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ export default class { | ||||
|    } | ||||
|  | ||||
|    static getStructure (connection) { | ||||
|       // return connection.raw('SELECT * FROM information_schema.TABLES ORDER BY TABLE_SCHEMA ASC, TABLE_NAME ASC'); | ||||
|       return connection | ||||
|          .select('*') | ||||
|          .schema('information_schema') | ||||
| @@ -14,5 +13,13 @@ export default class { | ||||
|          .run(); | ||||
|    } | ||||
|  | ||||
|    // TODO: SELECT * FROM `information_schema`.`COLUMNS` WHERE TABLE_SCHEMA='fepcomdb' AND TABLE_NAME='macchine' ORDER BY ORDINAL_POSITION; | ||||
|    static getTableColumns (connection, schema, table) { | ||||
|       return connection | ||||
|          .select('*') | ||||
|          .schema('information_schema') | ||||
|          .from('COLUMNS') | ||||
|          .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` }) | ||||
|          .orderBy({ ORDINAL_POSITION: 'ASC' }) | ||||
|          .run(); | ||||
|    } | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,8 @@ | ||||
|    <details class="accordion workspace-explorebar-database"> | ||||
|       <summary | ||||
|          class="accordion-header database-name pb-0" | ||||
|          :class="{'text-bold': breadcrumbs.database === database.name}" | ||||
|          @click="changeBreadcrumbs({database: database.name, table:null})" | ||||
|          :class="{'text-bold': breadcrumbs.schema === database.name}" | ||||
|          @click="changeBreadcrumbs({schema: database.name, table:null})" | ||||
|       > | ||||
|          <i class="icon material-icons md-18 mr-1">navigate_next</i> | ||||
|          <i class="material-icons md-18 mr-1">view_agenda</i> | ||||
| @@ -16,8 +16,8 @@ | ||||
|                   v-for="table of database.tables" | ||||
|                   :key="table.TABLE_NAME" | ||||
|                   class="menu-item" | ||||
|                   :class="{'text-bold': breadcrumbs.database === database.name && breadcrumbs.table === table.TABLE_NAME}" | ||||
|                   @click="changeBreadcrumbs({database: database.name, table: table.TABLE_NAME})" | ||||
|                   :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}" | ||||
|                   @click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})" | ||||
|                > | ||||
|                   <a class="table-name"> | ||||
|                      <i class="material-icons md-18 mr-1">grid_on</i> | ||||
|   | ||||
| @@ -22,8 +22,8 @@ | ||||
|                <div v-if="results.rows"> | ||||
|                   {{ $t('word.results') }}: <b>{{ results.rows.length }}</b> | ||||
|                </div> | ||||
|                <div v-if="workspace.breadcrumbs.database"> | ||||
|                   {{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b> | ||||
|                <div v-if="workspace.breadcrumbs.schema"> | ||||
|                   {{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b> | ||||
|                </div> | ||||
|             </div> | ||||
|          </div> | ||||
| @@ -76,7 +76,7 @@ export default { | ||||
|          const params = { | ||||
|             uid: this.connection.uid, | ||||
|             query: this.query, | ||||
|             database: this.workspace.breadcrumbs.database | ||||
|             schema: this.workspace.breadcrumbs.schema | ||||
|          }; | ||||
|  | ||||
|          try { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|                <button | ||||
|                   class="btn btn-link btn-sm" | ||||
|                   :class="{'loading':isQuering}" | ||||
|                   @click="runQuery" | ||||
|                   @click="getTableData" | ||||
|                > | ||||
|                   <span>{{ $t('word.refresh') }}</span> | ||||
|                   <i class="material-icons ml-1">refresh</i> | ||||
| @@ -33,7 +33,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Connection from '@/ipc-api/Connection'; | ||||
| import Structure from '@/ipc-api/Structure'; | ||||
| import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; | ||||
| import { mapGetters, mapActions } from 'vuex'; | ||||
|  | ||||
| @@ -50,6 +50,7 @@ export default { | ||||
|       return { | ||||
|          isQuering: false, | ||||
|          results: {}, | ||||
|          fields: {}, | ||||
|          lastTable: null | ||||
|       }; | ||||
|    }, | ||||
| @@ -62,45 +63,54 @@ export default { | ||||
|       }, | ||||
|       isSelected () { | ||||
|          return this.workspace.selected_tab === 1; | ||||
|       }, | ||||
|       query () { | ||||
|          return `SELECT * FROM \`${this.table}\` LIMIT 1000`;// TODO: use query builder | ||||
|       } | ||||
|    }, | ||||
|    watch: { | ||||
|       table: function () { | ||||
|          if (this.isSelected) { | ||||
|             this.runQuery(); | ||||
|             this.getTableData(); | ||||
|             this.lastTable = this.table; | ||||
|          } | ||||
|       }, | ||||
|       isSelected: function (val) { | ||||
|          if (val && this.lastTable !== this.table) { | ||||
|             this.runQuery(); | ||||
|             this.getTableData(); | ||||
|             this.lastTable = this.table; | ||||
|          } | ||||
|       } | ||||
|    }, | ||||
|    created () { | ||||
|       this.runQuery(); | ||||
|       this.getTableData(); | ||||
|    }, | ||||
|    methods: { | ||||
|       ...mapActions({ | ||||
|          addNotification: 'notifications/addNotification' | ||||
|       }), | ||||
|       async runQuery () { | ||||
|       async getTableData () { | ||||
|          if (!this.table) return; | ||||
|          this.isQuering = true; | ||||
|          this.results = {}; | ||||
|  | ||||
|          const params = { | ||||
|             uid: this.connection.uid, | ||||
|             query: this.query, | ||||
|             database: this.workspace.breadcrumbs.database | ||||
|             schema: this.workspace.breadcrumbs.schema, | ||||
|             table: this.workspace.breadcrumbs.table | ||||
|          }; | ||||
|  | ||||
|          try { | ||||
|             const { status, response } = await Connection.rawQuery(params); | ||||
|             const { status, response } = await Structure.getTableColumns(params); | ||||
|             if (status === 'success') | ||||
|                this.fields = response.rows; | ||||
|             else | ||||
|                this.addNotification({ status: 'error', message: response }); | ||||
|          } | ||||
|          catch (err) { | ||||
|             this.addNotification({ status: 'error', message: err.stack }); | ||||
|          } | ||||
|  | ||||
|          try { | ||||
|             const { status, response } = await Structure.getTableData(params); | ||||
|             console.log(status, response); | ||||
|             if (status === 'success') | ||||
|                this.results = response; | ||||
|             else | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/renderer/ipc-api/Structure.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/renderer/ipc-api/Structure.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| 'use strict'; | ||||
| import { ipcRenderer } from 'electron'; | ||||
|  | ||||
| export default class { | ||||
|    static getTableColumns (params) { | ||||
|       return ipcRenderer.invoke('getTableColumns', params); | ||||
|    } | ||||
|  | ||||
|    static getTableData (params) { | ||||
|       return ipcRenderer.invoke('getTableData', params); | ||||
|    } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user