From b2dbf67e7f1ab7d70957986ae14bb4bf5b49e92b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 3 May 2019 17:53:52 +0200 Subject: [PATCH 1/3] Allow more powerful queries for Realms/Users tables --- .../ServerAdministration/RealmsTable/RealmsTable.tsx | 5 ++++- src/ui/ServerAdministration/RealmsTable/index.tsx | 10 +++++++--- .../ServerAdministration/UsersTable/UsersTable.tsx | 5 ++++- src/ui/ServerAdministration/UsersTable/index.tsx | 7 +++++-- .../shared/FilterableTable/index.tsx | 3 +++ src/ui/ServerAdministration/utils.tsx | 12 ++++++++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx b/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx index eff2c6262..b095f49c8 100644 --- a/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx +++ b/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx @@ -52,6 +52,7 @@ export const RealmsTable = ({ onSearchStringChange, realms, searchString, + queryError, selectedRealms, onRealmSizeRecalculate, shouldShowRealmSize, @@ -68,6 +69,7 @@ export const RealmsTable = ({ onSearchStringChange: (query: string) => void; realms: Realm.Results; searchString: string; + queryError?: Error; selectedRealms: RealmFile[]; onRealmSizeRecalculate: (realm: RealmFile) => void; shouldShowRealmSize: boolean; @@ -81,8 +83,9 @@ export const RealmsTable = ({ onElementDoubleClick={onRealmOpened} onElementsDeselection={onRealmsDeselection} onSearchStringChange={onSearchStringChange} - searchPlaceholder="Search Realms" + searchPlaceholder="Search Realms (start with ! to write a verbatim realm-js query)" searchString={searchString} + queryError={queryError} selectedElements={selectedRealms} isElementsEqual={(a, b) => a.path === b.path} > diff --git a/src/ui/ServerAdministration/RealmsTable/index.tsx b/src/ui/ServerAdministration/RealmsTable/index.tsx index 8dd9d1390..92759a2cf 100644 --- a/src/ui/ServerAdministration/RealmsTable/index.tsx +++ b/src/ui/ServerAdministration/RealmsTable/index.tsx @@ -60,6 +60,7 @@ export interface IRealmTableContainerState { /** Prevents spamming the server too badly */ deletionProgress?: IDeletionProgress; searchString: string; + queryError?: Error; // TODO: Update this once Realm JS has better support for Sets selectedRealms: RealmFile[]; showPartialRealms: boolean; @@ -84,6 +85,7 @@ class RealmsTableContainer extends React.Component< showPartialRealms: boolean, showSystemRealms: boolean, ) => { + let queryError: Error | undefined; let realms = adminRealm .objects('RealmFile') .sorted('createdAt'); @@ -97,6 +99,7 @@ class RealmsTableContainer extends React.Component< try { realms = realms.filtered(filterQuery); } catch (err) { + queryError = err; // tslint:disable-next-line:no-console console.warn(`Could not filter on "${filterQuery}"`, err); } @@ -118,7 +121,7 @@ class RealmsTableContainer extends React.Component< ].join(' AND '), ); } - return realms; + return { realms, queryError }; }, ); @@ -283,7 +286,7 @@ class RealmsTableContainer extends React.Component< }; private renderTable() { - const realms = this.realms( + const { realms, queryError } = this.realms( this.props.adminRealm, this.state.searchString, this.state.showPartialRealms, @@ -311,6 +314,7 @@ class RealmsTableContainer extends React.Component< onSearchStringChange={this.onSearchStringChange} realms={realms} searchString={this.state.searchString} + queryError={queryError} selectedRealms={validSelectedRealms} deletionProgress={this.state.deletionProgress} onRealmSizeRecalculate={this.onRealmSizeRecalculate} @@ -369,7 +373,7 @@ class RealmsTableContainer extends React.Component< } private getRealmsBetween(realmA: RealmFile, realmB: RealmFile) { - const realms = this.realms( + const { realms } = this.realms( this.props.adminRealm, this.state.searchString, this.state.showPartialRealms, diff --git a/src/ui/ServerAdministration/UsersTable/UsersTable.tsx b/src/ui/ServerAdministration/UsersTable/UsersTable.tsx index c56b38624..d0a202662 100644 --- a/src/ui/ServerAdministration/UsersTable/UsersTable.tsx +++ b/src/ui/ServerAdministration/UsersTable/UsersTable.tsx @@ -67,6 +67,7 @@ export const UsersTable = ({ searchString, selection, users, + queryError, }: { getUsersRealms: (user: User) => Realm.Results; isChangePasswordOpen: boolean; @@ -93,6 +94,7 @@ export const UsersTable = ({ users: Realm.Results; searchString: string; onSearchStringChange: (query: string) => void; + queryError?: Error; }) => { return (
@@ -103,9 +105,10 @@ export const UsersTable = ({ onElementClick={onUserClick} onElementsDeselection={onUsersDeselection} onSearchStringChange={onSearchStringChange} - searchPlaceholder="Search users" + searchPlaceholder="Search users (start with ! to write a verbatim realm-js query)" searchString={searchString} selectedElements={selection ? [selection.user] : []} + queryError={queryError} > { + let queryError: Error | undefined; let users = adminRealm.objects('User').sorted('userId'); // Filter if a search string is specified if (searchString && searchString !== '') { @@ -70,6 +71,7 @@ class UsersTableContainer extends React.Component< try { users = users.filtered(filterQuery); } catch (err) { + queryError = err; // tslint:disable-next-line:no-console console.warn(`Could not filter on "${filterQuery}"`, err); } @@ -81,7 +83,7 @@ class UsersTableContainer extends React.Component< "NOT userId == '__admin' AND NOT accounts.provider BEGINSWITH 'jwt/central'", ); } - return users; + return { users, queryError }; }, ); @@ -110,7 +112,7 @@ class UsersTableContainer extends React.Component< return null; } - const users = this.users( + const { users, queryError } = this.users( this.props.adminRealm, this.state.searchString, this.state.showSystemUsers, @@ -142,6 +144,7 @@ class UsersTableContainer extends React.Component< searchString={this.state.searchString} selection={selection} users={users} + queryError={queryError} /> ); } diff --git a/src/ui/ServerAdministration/shared/FilterableTable/index.tsx b/src/ui/ServerAdministration/shared/FilterableTable/index.tsx index e81a8b8b1..ebb4573ef 100644 --- a/src/ui/ServerAdministration/shared/FilterableTable/index.tsx +++ b/src/ui/ServerAdministration/shared/FilterableTable/index.tsx @@ -38,6 +38,7 @@ export interface IFilterableTableProps { onSearchStringChange: (searchString: string) => void; searchPlaceholder: string; searchString: string; + queryError?: Error; selectedElements: E[]; } @@ -53,12 +54,14 @@ export const FilterableTable = ({ searchPlaceholder, searchString, selectedElements, + queryError, }: IFilterableTableProps) => (
diff --git a/src/ui/ServerAdministration/utils.tsx b/src/ui/ServerAdministration/utils.tsx index c82ec91da..774ee03b1 100644 --- a/src/ui/ServerAdministration/utils.tsx +++ b/src/ui/ServerAdministration/utils.tsx @@ -55,5 +55,13 @@ export const shortenRealmPath = (path: string) => { export const querySomeFieldContainsText = ( fields: string[], textToContain: string, -) => - fields.map(field => `${field} CONTAINS[c] "${textToContain}"`).join(' OR '); +): string => { + // If query starts with !, just execute as is + if (textToContain.indexOf('!') === 0) { + return textToContain.substring(1); + } else { + return fields + .map(field => `${field} CONTAINS[c] "${textToContain}"`) + .join(' OR '); + } +}; From f31c5408e194b935ea92f2045206db7184c941c1 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 3 Jun 2019 14:29:25 +0200 Subject: [PATCH 2/3] Review feedback --- package-lock.json | 119 +----------------- package.json | 6 - .../RealmsTable/RealmsTable.tsx | 21 +++- .../RealmsTable/index.tsx | 4 +- .../ServerAdministration/UsersTable/index.tsx | 4 +- .../shared/FilterableTable/index.tsx | 6 + src/ui/ServerAdministration/utils.tsx | 18 ++- src/ui/reusable/QuerySearch/QuerySearch.tsx | 76 ++++++----- src/ui/reusable/QuerySearch/index.tsx | 2 + 9 files changed, 90 insertions(+), 166 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1762d938..66559fe11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1320,7 +1320,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -6850,115 +6850,6 @@ "integrity": "sha1-pls0RZrWNnrbs3B6gqPJ+RYWcDA=", "dev": true }, - "husky": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", - "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.7", - "execa": "^1.0.0", - "find-up": "^3.0.0", - "get-stdin": "^6.0.0", - "is-ci": "^2.0.0", - "pkg-dir": "^3.0.0", - "please-upgrade-node": "^3.1.1", - "read-pkg": "^4.0.1", - "run-node": "^1.0.0", - "slash": "^2.0.0" - }, - "dependencies": { - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11985,7 +11876,7 @@ "dependencies": { "convert-source-map": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", "dev": true } @@ -12030,12 +11921,6 @@ "once": "^1.3.0" } }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", diff --git a/package.json b/package.json index bf8eb2cf3..3f5b33cc1 100644 --- a/package.json +++ b/package.json @@ -214,7 +214,6 @@ "faker": "^4.1.0", "file-loader": "^3.0.1", "hard-source-webpack-plugin": "^0.13.1", - "husky": "^1.3.1", "js-yaml": "^3.13.1", "jsdom": "^13.2.0", "lint-staged": "^8.1.5", @@ -254,10 +253,5 @@ }, "engines": { "node": "^8" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } } } diff --git a/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx b/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx index b095f49c8..be68c3717 100644 --- a/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx +++ b/src/ui/ServerAdministration/RealmsTable/RealmsTable.tsx @@ -16,6 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// +import * as electron from 'electron'; import * as React from 'react'; import { Column } from 'react-virtualized'; import { Button } from 'reactstrap'; @@ -39,6 +40,22 @@ const FilterableRealmTable: React.ComponentType< IFilterableTableProps > = FilterableTable; +const onQueryHelp = () => { + const url = + 'https://realm.io/docs/javascript/latest/api/tutorial-query-language.html'; + electron.shell.openExternal(url); +}; + +const queryHelpTooltip = ( +
+ Start a query with ! to pass in a verbatim realm-js query. For example: +
    +
  • !path = "/default"
  • +
  • !userId ENDSWITH "123"
  • +
+
+); + export const RealmsTable = ({ deletionProgress, getRealmPermissions, @@ -84,6 +101,8 @@ export const RealmsTable = ({ onElementsDeselection={onRealmsDeselection} onSearchStringChange={onSearchStringChange} searchPlaceholder="Search Realms (start with ! to write a verbatim realm-js query)" + onQueryHelp={onQueryHelp} + queryHelpTooltip={queryHelpTooltip} searchString={searchString} queryError={queryError} selectedElements={selectedRealms} @@ -137,4 +156,4 @@ export const RealmsTable = ({ />
); -}; +}; \ No newline at end of file diff --git a/src/ui/ServerAdministration/RealmsTable/index.tsx b/src/ui/ServerAdministration/RealmsTable/index.tsx index 92759a2cf..9af10cf74 100644 --- a/src/ui/ServerAdministration/RealmsTable/index.tsx +++ b/src/ui/ServerAdministration/RealmsTable/index.tsx @@ -32,7 +32,7 @@ import { IRealmStateSize, MetricsRealmProvider, } from '../MetricsRealm'; -import { querySomeFieldContainsText } from '../utils'; +import { getQueryForFields } from '../utils'; import { RealmsTable } from './RealmsTable'; @@ -92,7 +92,7 @@ class RealmsTableContainer extends React.Component< // Filter if a search string is specified if (searchString || searchString !== '') { - const filterQuery = querySomeFieldContainsText( + const filterQuery = getQueryForFields( ['path', 'realmType', 'owner.accounts.providerId'], searchString, ); diff --git a/src/ui/ServerAdministration/UsersTable/index.tsx b/src/ui/ServerAdministration/UsersTable/index.tsx index 46f79be86..af4b5ae15 100644 --- a/src/ui/ServerAdministration/UsersTable/index.tsx +++ b/src/ui/ServerAdministration/UsersTable/index.tsx @@ -25,7 +25,7 @@ import * as ros from '../../../services/ros'; import { store } from '../../../store'; import { showError } from '../../reusable/errors'; import { withAdminRealm } from '../AdminRealm'; -import { querySomeFieldContainsText } from '../utils'; +import { getQueryForFields } from '../utils'; import { UsersTable } from './UsersTable'; export interface ISelection { @@ -64,7 +64,7 @@ class UsersTableContainer extends React.Component< let users = adminRealm.objects('User').sorted('userId'); // Filter if a search string is specified if (searchString && searchString !== '') { - const filterQuery = querySomeFieldContainsText( + const filterQuery = getQueryForFields( ['userId', 'accounts.providerId', 'metadata.key', 'metadata.value'], searchString, ); diff --git a/src/ui/ServerAdministration/shared/FilterableTable/index.tsx b/src/ui/ServerAdministration/shared/FilterableTable/index.tsx index ebb4573ef..f42e454a9 100644 --- a/src/ui/ServerAdministration/shared/FilterableTable/index.tsx +++ b/src/ui/ServerAdministration/shared/FilterableTable/index.tsx @@ -40,6 +40,8 @@ export interface IFilterableTableProps { searchString: string; queryError?: Error; selectedElements: E[]; + onQueryHelp?: () => void; + queryHelpTooltip?: JSX.Element; } export const FilterableTable = ({ @@ -55,12 +57,16 @@ export const FilterableTable = ({ searchString, selectedElements, queryError, + onQueryHelp, + queryHelpTooltip, }: IFilterableTableProps) => (
diff --git a/src/ui/ServerAdministration/utils.tsx b/src/ui/ServerAdministration/utils.tsx index 774ee03b1..2c2c60d6c 100644 --- a/src/ui/ServerAdministration/utils.tsx +++ b/src/ui/ServerAdministration/utils.tsx @@ -52,16 +52,24 @@ export const shortenRealmPath = (path: string) => { } }; -export const querySomeFieldContainsText = ( +export const getQueryForFields = ( fields: string[], textToContain: string, ): string => { // If query starts with !, just execute as is if (textToContain.indexOf('!') === 0) { return textToContain.substring(1); - } else { - return fields - .map(field => `${field} CONTAINS[c] "${textToContain}"`) - .join(' OR '); } + + // Otherwise, generate an OR query to find the text in any of the fields. + return querySomeFieldContainsText(fields, textToContain); +}; + +const querySomeFieldContainsText = ( + fields: string[], + textToContain: string, +): string => { + return fields + .map(field => `${field} CONTAINS[c] "${textToContain}"`) + .join(' OR '); }; diff --git a/src/ui/reusable/QuerySearch/QuerySearch.tsx b/src/ui/reusable/QuerySearch/QuerySearch.tsx index c7166ab2f..3a1b597a6 100644 --- a/src/ui/reusable/QuerySearch/QuerySearch.tsx +++ b/src/ui/reusable/QuerySearch/QuerySearch.tsx @@ -24,11 +24,13 @@ import { InputGroupAddon, Popover, PopoverBody, + UncontrolledTooltip, } from 'reactstrap'; export interface IQuerySearchProps { onQueryChange: (query: string) => void; onQueryHelp?: () => void; + queryHelpTooltip?: JSX.Element; onQueryBlur?: React.EventHandler; onQueryFocus?: React.EventHandler; query: string; @@ -52,37 +54,45 @@ export const QuerySearch = ({ inputRef, inputElement, isPopoverOpen, -}: IQuerySearchProps) => ( -
- - { - onQueryChange(e.target.value); - }} - onFocus={onQueryFocus} - onBlur={onQueryBlur} - placeholder={placeholder} - value={query} - invalid={!!queryError} - innerRef={inputRef} - /> - {onQueryHelp && ( - -