diff --git a/dist/DatastoreSearchSql.js b/dist/DatastoreSearchSql.js index 4668fd7..f3bc804 100644 --- a/dist/DatastoreSearchSql.js +++ b/dist/DatastoreSearchSql.js @@ -7,7 +7,7 @@ exports.default = void 0; require("./i18n/i18n"); -var _react = _interopRequireDefault(require("react")); +var _react = _interopRequireWildcard(require("react")); var _formik = require("formik"); @@ -15,9 +15,31 @@ var _reactDatePicker = _interopRequireDefault(require("react-date-picker")); var _reactI18next = require("react-i18next"); +var _QueryBuilder = _interopRequireDefault(require("./QueryBuilder")); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } + +function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + function DatastoreSearchSql(props) { + var _useState = (0, _react.useState)(false), + _useState2 = _slicedToArray(_useState, 2), + showQueryBuilder = _useState2[0], + setShowQueryBuilder = _useState2[1]; + + var _useState3 = (0, _react.useState)("SELECT * FROM \"".concat(props.resource.id, "\" ORDER BY \"_id\" ASC LIMIT 100")), + _useState4 = _slicedToArray(_useState3, 2), + query = _useState4[0], + setQuery = _useState4[1]; + var resource = JSON.parse(JSON.stringify(props.resource)); var dateFields = resource.schema.fields.filter(function (field) { return field.type && field.type.includes('date'); @@ -119,15 +141,21 @@ function DatastoreSearchSql(props) { var datastoreUrl = encodeURI(props.apiUrl + "datastore_search_sql?sql=".concat(sqlQueryString)); // Trigger Redux action resource.api = datastoreUrl; + setQuery(sqlQueryString); props.action(resource); } function handleReset() { // Initial api url should be `datastore_search` without any options. resource.api = props.apiUrl + "datastore_search?resource_id=".concat(resource.id, "&limit=100"); + setQuery("SELECT * FROM \"".concat(props.resource.id, "\" ORDER BY \"_id\" ASC LIMIT 100")); props.action(resource); } + function QueryBuiderToggle() { + setShowQueryBuilder(!showQueryBuilder); + } + return _react.default.createElement(_formik.Formik, { initialValues: { rules: resource.rules || [], @@ -150,7 +178,7 @@ function DatastoreSearchSql(props) { var values = _ref.values, setFieldValue = _ref.setFieldValue, handleReset = _ref.handleReset; - return _react.default.createElement(_formik.Form, { + return _react.default.createElement(_react.default.Fragment, null, _react.default.createElement(_formik.Form, { className: "form-inline dq-main-container" }, _react.default.createElement("div", { className: "dq-heading" @@ -159,13 +187,15 @@ function DatastoreSearchSql(props) { }, _react.default.createElement(_formik.Field, { name: "date.fieldName", component: "select", - className: "form-control" + className: "form-control", + "aria-label": "Choose date field" }, dateFields.map(function (field, index) { return _react.default.createElement("option", { value: field.name, key: "dateField".concat(index) }, field.title || field.name); })), _react.default.createElement(_reactDatePicker.default, { + calendarAriaLabel: "select start date from calendar", value: values.date.startDate, clearIcon: "X", nativeInputAriaLabel: "Start date input box", @@ -175,11 +205,13 @@ function DatastoreSearchSql(props) { onChange: function onChange(val) { return setFieldValue("date.startDate", val); }, - format: "yyyy-MM-dd" + format: "yyyy-MM-dd", + altInput: true }), _react.default.createElement("span", { className: "fa fa-long-arrow-right", "aria-hidden": "true" }), _react.default.createElement(_reactDatePicker.default, { + calendarAriaLabel: "select end date from calendar", value: values.date.endDate, clearIcon: "X", nativeInputAriaLabel: "End date input box", @@ -280,9 +312,16 @@ function DatastoreSearchSql(props) { type: "submit", className: "btn btn-primary reset-button", onClick: handleReset - }, t('Reset')))); + }, t('Reset')), _react.default.createElement("button", { + type: "button", + className: "btn btn-default query-builder-button ".concat(showQueryBuilder ? 'active' : ''), + onClick: QueryBuiderToggle + }, t('Query Builder')))); } - })); + })), showQueryBuilder ? _react.default.createElement(_QueryBuilder.default, { + apiUrl: props.apiUrl, + queryString: query + }) : null); } }); } diff --git a/dist/QueryBuilder.js b/dist/QueryBuilder.js new file mode 100644 index 0000000..31f2c1e --- /dev/null +++ b/dist/QueryBuilder.js @@ -0,0 +1,103 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +require("./i18n/i18n"); + +var _react = _interopRequireWildcard(require("react")); + +var _reactTabsRedux = require("react-tabs-redux"); + +var _reactHighlight = _interopRequireDefault(require("react-highlight")); + +var _reactI18next = require("react-i18next"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } + +function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +function QueryBuilder(props) { + var _useTranslation = (0, _reactI18next.useTranslation)(), + t = _useTranslation.t; + + var queryString = props.queryString; + + var _useState = (0, _react.useState)('Copy'), + _useState2 = _slicedToArray(_useState, 2), + copyButton = _useState2[0], + setcopyButton = _useState2[1]; + + var apiUrl = props.apiUrl; + var datastoreUrl = encodeURI(apiUrl + "datastore_search_sql?sql=".concat(queryString)); + var snippetSets = [{ + lang: 'cUrl', + format: 'bash', + snippet: "curl -L -s \"".concat(datastoreUrl, "\"") + }, { + lang: 'Python', + format: 'python', + snippet: "import requests\nfrom urllib import parse\n\nsql_query = '''".concat(queryString, "'''\nparams = {'sql': sql_query}\n\ntry:\n resposne = requests.get('").concat(apiUrl, "datastore_search_sql', params = parse.urlencode(params))\n data = resposne.json()[\"result\"]\n print(data) # Printing data\nexcept requests.exceptions.RequestException as e:\n print(e.response.text)") + }, { + lang: 'Javascript', + format: 'javascript', + snippet: "const sql_query = `".concat(queryString, "`\n\nfetch('").concat(apiUrl, "datastore_search_sql?sql=' + encodeURI(sql_query))\n .then((response) => response.json())\n .then((data) => {\n console.log('Success:', data['result']);\n })\n .catch((error) => {\n console.error('Error:', error);\n });") + }, { + lang: 'R', + format: 'r', + snippet: "library(jsonlite)\n\nencoded_query <- '".concat(datastoreUrl, "'\nreturned <- fromJSON(encoded_query)\n\ndf <- returned$result$records\nprint(df)") + }, { + lang: 'Pandas', + format: 'python', + snippet: "# Install pandas package if you don't have it already\n# pip install pandas\n\n# Get data and convert into dataframe\nimport pandas as pd\nimport requests\nfrom urllib import parse\n\nsql_query = '''".concat(queryString, "'''\nparams = {'sql': sql_query}\n\ntry:\n resposne = requests.get('").concat(apiUrl, "datastore_search_sql', params = parse.urlencode(params))\n data = resposne.json()[\"result\"]\n df = pd.DataFrame(data[\"records\"])\n print(df) # Dataframe\nexcept requests.exceptions.RequestException as e:\n print(e.response.text)") + }]; + + function handleCopy(snippet) { + navigator.clipboard.writeText(snippet); + setcopyButton('Copied'); + } + + function onTabChange() { + setcopyButton('Copy'); + } + + return _react.default.createElement("div", { + className: "dq-querybuilder" + }, _react.default.createElement("h3", null, t('Integrate into your tools')), _react.default.createElement(_reactTabsRedux.Tabs, null, snippetSets.map(function (item, key) { + return _react.default.createElement(_reactTabsRedux.TabLink, { + onClick: onTabChange, + to: item.lang, + key: key, + className: "mr-4 tab-".concat(item.lang) + }, item.lang); + }), snippetSets.map(function (item, key) { + return _react.default.createElement(_reactTabsRedux.TabContent, { + key: key, + for: item.lang + }, _react.default.createElement("button", { + className: "snippet-copy", + style: { + float: 'right' + }, + onClick: function onClick() { + return handleCopy(item.snippet); + } + }, copyButton), _react.default.createElement(_reactHighlight.default, { + language: item.format, + className: "language-".concat(item.format) + }, item.snippet)); + }))); +} + +var _default = QueryBuilder; +exports.default = _default; \ No newline at end of file diff --git a/package.json b/package.json index 6a33120..35a000b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "react": "^16.8.6", "react-date-picker": "^7.7.0", "react-dom": "^16.8.6", - "react-i18next": "^11.3.0" + "react-highlight": "^0.14.0", + "react-i18next": "^11.3.0", + "react-tabs-redux": "^4.0.0" }, "babel": { "presets": [ diff --git a/src/DatastoreSearchSql.js b/src/DatastoreSearchSql.js index adf8736..76548ca 100644 --- a/src/DatastoreSearchSql.js +++ b/src/DatastoreSearchSql.js @@ -1,12 +1,16 @@ import "./i18n/i18n" -import React from 'react'; -import {Formik, Form, FieldArray, Field} from 'formik' +import React, { useState } from 'react'; +import { Formik, Form, FieldArray, Field } from 'formik' import DatePicker from 'react-date-picker' -import {useTranslation} from "react-i18next" +import { useTranslation } from "react-i18next" +import QueryBuilder from './QueryBuilder' function DatastoreSearchSql(props) { + const [showQueryBuilder, setShowQueryBuilder] = useState(false) + const [query, setQuery] = useState(`SELECT * FROM "${props.resource.id}" ORDER BY "_id" ASC LIMIT 100`) + const resource = JSON.parse(JSON.stringify(props.resource)) const dateFields = resource.schema.fields.filter(field => field.type && field.type.includes('date')) @@ -16,12 +20,12 @@ function DatastoreSearchSql(props) { const { t } = useTranslation(); const operators = [ - {name: '=', label: '='}, - {name: '!=', label: '!='}, - {name: '<', label: '<'}, - {name: '>', label: '>'}, - {name: '<=', label: '<='}, - {name: '>=', label: '>='} + { name: '=', label: '=' }, + { name: '!=', label: '!=' }, + { name: '<', label: '<' }, + { name: '>', label: '>' }, + { name: '<=', label: '<=' }, + { name: '>=', label: '>=' } ] function validate(values) { @@ -39,7 +43,7 @@ function DatastoreSearchSql(props) { // we get number of total rows info. let sqlQueryString = `SELECT COUNT(*) OVER () AS _count, * FROM "${resource.id}" WHERE ` if (clonedValues.date.startDate) { - const rule = { combinator: 'AND', field: clonedValues.date.fieldName, operator: '>=', value: clonedValues.date.startDate} + const rule = { combinator: 'AND', field: clonedValues.date.fieldName, operator: '>=', value: clonedValues.date.startDate } let localDateTime = new Date(clonedValues.date.startDate); // Now, convert it into GMT considering offset let offset = localDateTime.getTimezoneOffset(); @@ -48,7 +52,7 @@ function DatastoreSearchSql(props) { clonedValues.rules.push(rule) } if (clonedValues.date.endDate) { - const rule = { combinator: 'AND', field: clonedValues.date.fieldName, operator: '<=', value: clonedValues.date.endDate} + const rule = { combinator: 'AND', field: clonedValues.date.fieldName, operator: '<=', value: clonedValues.date.endDate } let localDateTime = new Date(clonedValues.date.endDate); // Now, convert it into GMT considering offset let offset = localDateTime.getTimezoneOffset(); @@ -74,15 +78,21 @@ function DatastoreSearchSql(props) { const datastoreUrl = encodeURI(props.apiUrl + `datastore_search_sql?sql=${sqlQueryString}`) // Trigger Redux action resource.api = datastoreUrl + setQuery(sqlQueryString) props.action(resource) } function handleReset() { // Initial api url should be `datastore_search` without any options. resource.api = props.apiUrl + `datastore_search?resource_id=${resource.id}&limit=100` + setQuery(`SELECT * FROM "${props.resource.id}" ORDER BY "_id" ASC LIMIT 100`) props.action(resource) } + function QueryBuiderToggle() { + setShowQueryBuilder(!showQueryBuilder) + } + return ( validate(values) @@ -103,26 +113,32 @@ function DatastoreSearchSql(props) { handleReset() } render={({ values, setFieldValue, handleReset }) => ( -
-
- {defaultDateFieldName ? ( -
- - { dateFields.map((field, index) => ( - - ))} - - setFieldValue(`date.startDate`, val)} - format='yyyy-MM-dd' /> - - + +
+ {defaultDateFieldName ? ( +
+ + {dateFields.map((field, index) => ( + + ))} + + + setFieldValue(`date.startDate`, val)} + format='yyyy-MM-dd' + altInput={true} + /> + + -
- ) : ( - '' - )} - ( -
-
- {values.rules && values.rules.length > 0 ? ( - values.rules.map((rule, index) => ( -
- - - - - - {otherFields.map((field, index) => ( - - ))} - - - {operators.map((operator, index) => ( - - ))} - - - - -
- )) - ) : ( - - )} -
-
- - -
+ ) : ( + '' )} - /> - + ( +
+
+ {values.rules && values.rules.length > 0 ? ( + values.rules.map((rule, index) => ( +
+ + + + + + {otherFields.map((field, index) => ( + + ))} + + + {operators.map((operator, index) => ( + + ))} + + + + +
+ )) + ) : ( + + )} +
+
+ + + +
+
+ )} + /> + + + { + showQueryBuilder ? : null + } + )} /> ) diff --git a/src/QueryBuilder.js b/src/QueryBuilder.js new file mode 100644 index 0000000..ea86c74 --- /dev/null +++ b/src/QueryBuilder.js @@ -0,0 +1,117 @@ +import './i18n/i18n' +import React, { useState } from 'react'; +import { Tabs, TabLink, TabContent } from 'react-tabs-redux' +import Highlight from 'react-highlight' +import {useTranslation} from "react-i18next" + + + +function QueryBuilder(props) { + const { t } = useTranslation(); + + const queryString = props.queryString + const [copyButton, setcopyButton] = useState('Copy'); + + const apiUrl = props.apiUrl + const datastoreUrl = encodeURI(apiUrl + `datastore_search_sql?sql=${queryString}`) + + const snippetSets = [{ + lang: 'cUrl', + format: 'bash', + snippet: `curl -L -s "${datastoreUrl}"` + }, { + lang: 'Python', + format: 'python', + snippet: `import requests +from urllib import parse + +sql_query = '''${queryString}''' +params = {'sql': sql_query} + +try: + resposne = requests.get('${apiUrl}datastore_search_sql', params = parse.urlencode(params)) + data = resposne.json()["result"] + print(data) # Printing data +except requests.exceptions.RequestException as e: + print(e.response.text)` + }, + { + lang: 'Javascript', + format: 'javascript', + snippet: `const sql_query = \`${queryString}\` + +fetch('${apiUrl}datastore_search_sql?sql=' + encodeURI(sql_query)) + .then((response) => response.json()) + .then((data) => { + console.log('Success:', data['result']); + }) + .catch((error) => { + console.error('Error:', error); + });` + }, + { + lang: 'R', + format: 'r', + snippet: + `library(jsonlite) + +encoded_query <- '${datastoreUrl}' +returned <- fromJSON(encoded_query) + +df <- returned$result$records +print(df)` + }, + { + lang: 'Pandas', + format: 'python', + snippet: `# Install pandas package if you don't have it already +# pip install pandas + +# Get data and convert into dataframe +import pandas as pd +import requests +from urllib import parse + +sql_query = '''${queryString}''' +params = {'sql': sql_query} + +try: + resposne = requests.get('${apiUrl}datastore_search_sql', params = parse.urlencode(params)) + data = resposne.json()["result"] + df = pd.DataFrame(data["records"]) + print(df) # Dataframe +except requests.exceptions.RequestException as e: + print(e.response.text)` + }] + + function handleCopy(snippet) { + navigator.clipboard.writeText(snippet) + setcopyButton('Copied'); + } + + function onTabChange() { + setcopyButton('Copy'); + } + + return ( +
+

{t('Integrate into your tools')}

+ + {snippetSets.map((item, key) => { + return {item.lang} + })} + + {snippetSets.map((item, key) => { + return + + + {item.snippet} + + + })} + +
+ ) +} +export default QueryBuilder \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index dc9ae78..c89635b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2160,6 +2160,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.6: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@4.2.x: version "4.2.1" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" @@ -4122,6 +4127,11 @@ hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" +highlight.js@^10.5.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -7571,6 +7581,13 @@ react-fit@^1.0.3: detect-element-overflow "^1.1.1" prop-types "^15.6.0" +react-highlight@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-highlight/-/react-highlight-0.14.0.tgz#5aefa5518baa580f96b68d48129d7a5d2dc0c9ef" + integrity sha512-kWE+KXOXidS7SABhVopOgMnowbI3RAfeGZbnrduLNlWrYAED8sycL9l/Fvw3w0PFpIIawB7mRDnyhDcM/cIIGA== + dependencies: + highlight.js "^10.5.0" + react-i18next@^11.3.0: version "11.3.0" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.3.0.tgz#8c827b084708924fd2e8c787aca78e0f7966fa44" @@ -7646,6 +7663,14 @@ react-scripts@3.0.1: optionalDependencies: fsevents "2.0.6" +react-tabs-redux@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/react-tabs-redux/-/react-tabs-redux-4.0.0.tgz#b803aa3e80ae317311a83b3740ae13a938247156" + integrity sha512-yk5afCpUlmWqXA/fhSi02YaCSlpFH5v6QlUiiVRx+V1OTyLDaZFDnARCflUoGkwIq00o7ryVWUXNkQ3UwAcebA== + dependencies: + classnames "^2.2.6" + prop-types "^15.6.2" + react@^16.8.6: version "16.9.0" resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"