new: [instance:search_all] Early work on search all feature

pull/72/head
mokaddem 2021-09-10 11:55:54 +02:00
parent 91db0afd9a
commit b3c25f0cae
20 changed files with 331 additions and 38 deletions

View File

@ -14,12 +14,16 @@ use Cake\Error\Debugger;
class AuthKeysController extends AppController
{
public $filterFields = ['Users.username', 'authkey', 'comment', 'Users.id'];
public $quickFilterFields = ['authkey', ['comment' => true]];
public $containFields = ['Users'];
public function index()
{
$this->CRUD->index([
'filters' => ['Users.username', 'authkey', 'comment', 'Users.id'],
'quickFilters' => ['authkey', 'comment'],
'contain' => ['Users'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
'exclude_fields' => ['authkey']
]);
$responsePayload = $this->CRUD->getResponsePayload();

View File

@ -10,17 +10,21 @@ use Cake\ORM\TableRegistry;
class BroodsController extends AppController
{
public $filterFields = ['Broods.name', 'Broods.uuid', 'Broods.url', 'Broods.description', 'Organisations.id', 'Broods.trusted', 'pull', 'authkey'];
public $quickFilterFields = [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]];
public $containFields = ['Organisations'];
public function index()
{
$this->CRUD->index([
'filters' => ['Broods.name', 'Broods.uuid', 'Broods.url', 'Broods.description', 'Organisations.id', 'Broods.trusted', 'pull', 'authkey'],
'quickFilters' => [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'pull',
]
],
'contain' => ['Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -494,7 +494,7 @@ class CRUDComponent extends Component
return $massagedFilters;
}
protected function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
{
$queryConditions = [];
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);

View File

@ -17,6 +17,10 @@ class NavigationComponent extends Component
public function initialize(array $config): void
{
$this->request = $config['request'];
}
public function beforeFilter($event)
{
$this->fullBreadcrumb = $this->genBreadcrumb();
$this->breadcrumb = $this->getBreadcrumb();
}

View File

@ -14,17 +14,21 @@ use Cake\Error\Debugger;
class EncryptionKeysController extends AppController
{
public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
public $quickFilterFields = ['encryption_key'];
public $containFields = ['Individuals', 'Organisations'];
public function index()
{
$this->CRUD->index([
'quickFilters' => ['encryption_key'],
'filters' => ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'],
'quickFilters' => $this->quickFilterFields,
'filters' => $this->filterFields,
'contextFilters' => [
'fields' => [
'type'
]
],
'contain' => ['Individuals', 'Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -16,7 +16,9 @@ use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'origin', 'comment'];
public $filterFields = ['scope', 'action', 'title', 'origin', 'comment'];
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
public $containFields = ['Users'];
public function beforeFilter(EventInterface $event)
{
@ -28,14 +30,14 @@ class InboxController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'scope',
]
],
'contain' => ['Users']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -12,17 +12,21 @@ use Cake\Http\Exception\ForbiddenException;
class IndividualsController extends AppController
{
public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position'];
public $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'];
public $containFields = ['Alignments' => 'Organisations'];
public function index()
{
$this->CRUD->index([
'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'],
'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'Alignments.type'
]
],
'contain' => ['Alignments' => 'Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -6,6 +6,7 @@ use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\ORM\TableRegistry;
use Cake\Event\EventInterface;
class InstanceController extends AppController
@ -18,7 +19,7 @@ class InstanceController extends AppController
public function home()
{
$this->set('md', file_get_contents(ROOT . '/README.md'));
// $this->set('md', file_get_contents(ROOT . '/README.md'));
}
public function status()
@ -29,6 +30,19 @@ class InstanceController extends AppController
return $this->RestResponse->viewData($data, 'json');
}
public function searchAll()
{
$searchValue = $this->request->getQuery('search');
$data = [];
if (!empty($searchValue)) {
$data = $this->Instance->searchAll($searchValue);
}
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($data, 'json');
}
$this->set('data', $data);
}
public function migrationIndex()
{
$migrationStatus = $this->Instance->getMigrationStatus();

View File

@ -9,11 +9,15 @@ use \Cake\Database\Expression\QueryExpression;
class MetaTemplateFieldsController extends AppController
{
public $quickFilterFields = ['field', 'type'];
public $filterFields = ['field', 'type', 'meta_template_id'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => ['field', 'type', 'meta_template_id'],
'quickFilters' => ['field', 'type']
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -9,6 +9,9 @@ use \Cake\Database\Expression\QueryExpression;
class MetaTemplatesController extends AppController
{
public $quickFilterFields = ['name', 'uuid', 'scope'];
public $filterFields = ['name', 'uuid', 'scope', 'namespace'];
public $containFields = ['MetaTemplateFields'];
public function update()
{
@ -34,8 +37,8 @@ class MetaTemplatesController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => ['name', 'uuid', 'scope', 'namespace'],
'quickFilters' => ['name', 'uuid', 'scope'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => ['scope'],
'custom' => [
@ -49,7 +52,7 @@ class MetaTemplatesController extends AppController
],
]
],
'contain' => ['MetaTemplateFields']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -13,13 +13,15 @@ use Cake\Http\Exception\ForbiddenException;
class OrganisationsController extends AppController
{
public $filters = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
public $containFields = ['Alignments' => 'Individuals'];
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'custom' => [
[
@ -57,7 +59,7 @@ class OrganisationsController extends AppController
]
],
],
'contain' => ['Alignments' => 'Individuals']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -16,7 +16,9 @@ use Cake\Http\Exception\ForbiddenException;
class OutboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'comment'];
public $filterFields = ['scope', 'action', 'title', 'comment'];
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
public $containFields = ['Users'];
public function beforeFilter(EventInterface $event)
{
@ -28,14 +30,14 @@ class OutboxController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'scope',
]
],
'contain' => ['Users']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -12,11 +12,15 @@ use Cake\Http\Exception\ForbiddenException;
class RolesController extends AppController
{
public $filterFields = ['name', 'uuid', 'perm_admin', 'Users.id'];
public $quickFilterFields = ['name'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => ['name', 'uuid', 'perm_admin', 'Users.id'],
'quickFilters' => ['name']
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -10,11 +10,16 @@ use Cake\Error\Debugger;
class SharingGroupsController extends AppController
{
public $filterFields = ['SharingGroups.uuid', 'SharingGroups.name', 'description', 'releasability', 'Organisations.name', 'Organisations.uuid'];
public $quickFilterFields = ['SharingGroups.uuid', ['SharingGroups.name' => true], ['description' => true], ['releasability' => true]];
public $containFields = ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]];
public function index()
{
$this->CRUD->index([
'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]],
'filters' => ['uuid', 'description', 'releasability', 'Organisations.name', 'Organisations.uuid']
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -9,12 +9,16 @@ use \Cake\Database\Expression\QueryExpression;
class UsersController extends AppController
{
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name'];
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
public $containFields = ['Individuals', 'Roles'];
public function index()
{
$this->CRUD->index([
'contain' => ['Individuals', 'Roles'],
'filters' => ['Users.email', 'uuid'],
'quickFilters' => ['uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'],
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -4,11 +4,16 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
use Migrations\Migrations;
use Cake\Http\Exception\MethodNotAllowedException;
class InstanceTable extends AppTable
{
public $seachAllTables = ['Broods', 'Individuals', 'Organisations', 'SharingGroups', 'Users', 'EncryptionKeys', ];
public function initialize(array $config): void
{
parent::initialize($config);
@ -19,6 +24,52 @@ class InstanceTable extends AppTable
return $validator;
}
public function searchAll($value)
{
$results = [];
foreach ($this->seachAllTables as $tableName) {
$controller = $this->getController($tableName);
$table = TableRegistry::get($tableName);
$query = $table->find();
$quickFilterOptions = $this->getQuickFiltersFieldsFromController($controller);
$containFields = $this->getContainFieldsFromController($controller);
if (empty($quickFilterOptions)) {
continue; // make sure we are filtering on something
}
$params = ['quickFilter' => $value];
$query = $controller->CRUD->setQuickFilters($params, $query, $quickFilterOptions);
if (!empty($containFields)) {
$query->contain($containFields);
}
$result = $query->limit(5)->all()->toList();
if (!empty($result)) {
$results[$tableName] = $result;
}
}
return $results;
}
public function getController($name)
{
$controllerName = "\\App\\Controller\\{$name}Controller";
if (!class_exists($controllerName)) {
throw new MethodNotAllowedException(__('Model `{0}` does not exists', $model));
}
$controller = new $controllerName;
return $controller;
}
public function getQuickFiltersFieldsFromController($controller)
{
return !empty($controller->quickFilterFields) ? $controller->quickFilterFields : [];
}
public function getContainFieldsFromController($controller)
{
return !empty($controller->containFields) ? $controller->containFields : [];
}
public function getMigrationStatus()
{
$migrations = new Migrations();

View File

@ -0,0 +1,10 @@
<ul>
<?php foreach ($data as $table => $tableResult): ?>
<li><strong><?= h($table) ?></strong></li>
<ul>
<?php foreach ($tableResult as $entry): ?>
<li><strong><?= h($entry['id']) ?></strong></li>
<?php endforeach; ?>
</ul>
<?php endforeach; ?>
</ul>

View File

@ -47,6 +47,7 @@ Configure::write('sidebarVariant', $sidebarVariant);
<?= $this->Html->script('popper.min.js') ?>
<?= $this->Html->script('bootstrap.bundle.js') ?>
<?= $this->Html->script('main.js') ?>
<?= $this->Html->script('utils.js') ?>
<?= $this->Html->script('bootstrap-helper.js') ?>
<?= $this->Html->script('api-helper.js') ?>
<?= $this->fetch('meta') ?>

View File

@ -99,9 +99,32 @@ function syntaxHighlightJson(json, indent) {
});
}
function performGlobalSearch() {
const $input = $('#globalSearch')
// const $resultContainer = $('.global-search-result-container')
const $resultContainer = $('.content')
const value = $input.val()
const endpoint = '/instance/searchAll'
const searchParams = new URLSearchParams({search: value});
const url = endpoint + '?' + searchParams
const options = {
statusNode: $resultContainer
}
$resultContainer.dropdown('show')
AJAXApi.quickFetchURL(url, options).then((theHTML) => {
$resultContainer.html(theHTML)
$input.focus()
})
}
var UI
$(document).ready(() => {
if (typeof UIFactory !== "undefined") {
UI = new UIFactory()
}
const debouncedGlobalSearch = debounce(performGlobalSearch, 400)
$('#globalSearch').keypress(debouncedGlobalSearch);
})

148
webroot/js/utils.js Normal file
View File

@ -0,0 +1,148 @@
// https://github.com/lodash/lodash/blob/master/debounce.js
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (typeof options === 'objects') {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}