Initial seeding of Searchlight UI
This repository is being seeded with a patch that has lived on Horizon master for the Liberty and Mitaka cycles. At the horizon mitaka mid-cycle, we decided to move this to a plugin for the Mitaka release. It was not possible to do so until several patch dependencies merged on horizon master. Those have now merged, so we are able to put this into its own repo.
This commit is contained in:
parent
1b08c890c0
commit
32883ae1d2
|
@ -0,0 +1,7 @@
|
|||
[run]
|
||||
branch = True
|
||||
source = searchlight_ui
|
||||
omit = searchlight_ui/openstack/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
|
@ -0,0 +1,45 @@
|
|||
# Set up globals
|
||||
globals:
|
||||
angular: false
|
||||
|
||||
extends: openstack
|
||||
|
||||
# Most environment options are not explicitly enabled or disabled, only
|
||||
# included here for completeness' sake. They are commented out, because the
|
||||
# global updates.py script would otherwise override them during a global
|
||||
# requirements synchronization.
|
||||
#
|
||||
# Individual projects should choose which platforms they deploy to.
|
||||
|
||||
env:
|
||||
# browser global variables.
|
||||
browser: true
|
||||
|
||||
# Adds all of the Jasmine testing global variables for version 1.3 and 2.0.
|
||||
jasmine: true
|
||||
|
||||
|
||||
# Below we adjust rules specific to horizon's usage of openstack's linting
|
||||
# rules, and its own plugin inclusions.
|
||||
rules:
|
||||
#############################################################################
|
||||
# Disabled Rules from eslint-config-openstack
|
||||
#############################################################################
|
||||
valid-jsdoc: 1
|
||||
no-undefined: 1
|
||||
brace-style: 1
|
||||
no-extra-parens: 1
|
||||
callback-return: 1
|
||||
block-scoped-var: 1
|
||||
|
||||
#############################################################################
|
||||
# Angular Plugin Customization
|
||||
#############################################################################
|
||||
|
||||
angular/controller-as-vm:
|
||||
- 1
|
||||
- "ctrl"
|
||||
|
||||
# Remove after migrating to angular 1.4 or later.
|
||||
angular/no-cookiestore:
|
||||
- 1
|
|
@ -0,0 +1,57 @@
|
|||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
.eggs
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
.testrepository
|
||||
.venv
|
||||
node_modules
|
||||
coverage*
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
||||
# Editors
|
||||
*~
|
||||
.*.swp
|
||||
.*sw?
|
||||
.idea
|
|
@ -0,0 +1,4 @@
|
|||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/searchlight-ui.git
|
|
@ -0,0 +1,3 @@
|
|||
# Format is:
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
|
@ -0,0 +1,7 @@
|
|||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
|
||||
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
|
||||
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
|
||||
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
|
@ -0,0 +1,21 @@
|
|||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps in documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Once those steps have been completed, changes to OpenStack
|
||||
should be submitted for review via the Gerrit tool, following
|
||||
the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/searchlight/
|
||||
|
||||
Further documentation on feature request and bug report processes may be
|
||||
found here:
|
||||
|
||||
http://docs.openstack.org/developer/searchlight/feature-requests-bugs.html
|
|
@ -0,0 +1,4 @@
|
|||
searchlight-ui Style Commandments
|
||||
===============================================
|
||||
|
||||
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
|
27
LICENSE
27
LICENSE
|
@ -1,3 +1,4 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
@ -173,29 +174,3 @@
|
|||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
include AUTHORS
|
||||
include ChangeLog
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
|
@ -1,2 +1,4 @@
|
|||
# searchlight-ui
|
||||
This is the basis of the new OpenStack Searchlight UI. It originally lived in a gerrit patch for Horizon, but was decided for mitaka to go ahead and create a separate plugin. See also: wiki.openstack.org/wiki/Searchlight
|
||||
This is the basis of the new OpenStack Searchlight UI. It originally lived in
|
||||
a gerrit patch for Horizon, but was decided for mitaka to go ahead and create
|
||||
a separate plugin. See also: wiki.openstack.org/wiki/Searchlight
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
==============
|
||||
searchlight-ui
|
||||
==============
|
||||
|
||||
Horizon panels and libraries for searchlight
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: http://docs.openstack.org/developer/searchlight
|
||||
* Source: http://git.openstack.org/cgit/openstack/searchlight-ui
|
||||
* Bugs: http://bugs.launchpad.net/searchlight
|
||||
|
||||
The Searchlight project provides indexing and search capabilities across
|
||||
OpenStack resources. Its goal is to achieve high performance and flexible
|
||||
querying combined with near real-time indexing.
|
||||
|
||||
Use the following resources to learn more:
|
||||
|
||||
* `Official Searchlight documentation * <http://docs.openstack.org/developer/searchlight/>`_
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Please see the searchlight-ui repository
|
||||
|
||||
Howto
|
||||
-----
|
||||
|
||||
1. Package the searchlight_ui by running::
|
||||
|
||||
python setup.py sdist
|
||||
|
||||
This will create a python egg in the dist folder, which can be used to
|
||||
install on the horizon machine or within horizon's python virtual
|
||||
environment::
|
||||
|
||||
cd ../horizon
|
||||
./tools/with_venv.sh pip install ../searchlight-ui/dist/searchlight-ui-0.0.0.tar.gz
|
||||
|
||||
2. Copy ``_1001_project_search_panel.py`` in
|
||||
``searchlight_ui/enabled`` directory
|
||||
to ``openstack_dashboard/local/enabled``::
|
||||
|
||||
cd <searchlight>
|
||||
cp -rv searchlight_ui/enabled/_1001_project_search_panel.py ../horizon/openstack_dashboard/local/enabled/
|
||||
|
||||
3. (Optional) Copy the policy file into horizon's policy files folder, and
|
||||
add this config ``POLICY_FILES``::
|
||||
|
||||
'searchlight_ui': 'searchlight_ui',
|
||||
|
||||
4. Django has a compressor feature that performs many enhancements for the
|
||||
delivery of static files. If the compressor feature is enabled in your
|
||||
environment (``COMPRESS_OFFLINE = True``), run the following commands::
|
||||
|
||||
$ ./manage.py collectstatic
|
||||
$ ./manage.py compress
|
||||
|
||||
5. Finally restart your web server to enable searchlight-ui
|
||||
in your Horizon::
|
||||
|
||||
$ sudo service apache2 restart
|
|
@ -0,0 +1,22 @@
|
|||
========================================
|
||||
Searchlight UI dashboard devstack plugin
|
||||
========================================
|
||||
|
||||
This directory contains the searchlight-ui devstack plugin.
|
||||
|
||||
To enable the plugin, add the following to your local.conf:
|
||||
|
||||
enable_plugin searchlight-ui <searchlight-ui GITURL> [GITREF]
|
||||
|
||||
where
|
||||
|
||||
<searchlight-ui GITURL> is the URL of a searchlight-ui repository
|
||||
[GITREF] is an optional git ref (branch/ref/tag). The default is master.
|
||||
|
||||
For example:
|
||||
|
||||
enable_plugin searchlight-ui https://git.openstack.org/openstack/searchlight-ui
|
||||
|
||||
Once you enable the plugin in your local.conf, ensure ``horizon``,
|
||||
``searchlight-api``, and ``searchlight-listener`` services are enabled. If they
|
||||
are enabled, searchlight-ui will be enabled automatically.
|
|
@ -0,0 +1,35 @@
|
|||
function searchlight_ui_install {
|
||||
setup_develop $SEARCHLIGHT_UI_DIR
|
||||
}
|
||||
|
||||
function searchlight_ui_dashboard_configure {
|
||||
cp $SEARCHLIGHT_UI_DIR_ENABLE_FILE \
|
||||
$HORIZON_DIR/openstack_dashboard/local/enabled/
|
||||
}
|
||||
|
||||
if is_service_enabled horizon && is_service_enabled search; then
|
||||
if [[ "$1" == "stack" && "$2" == "install" ]]; then
|
||||
# Perform installation of service source
|
||||
echo_summary "Installing searchlight-ui"
|
||||
searchlight_ui_install
|
||||
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
|
||||
echo_summary "Configuring searchlight-ui"
|
||||
searchlight_ui_dashboard_configure
|
||||
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
|
||||
# Initialize (nothing for now)
|
||||
echo_summary "Initializing searchlight-ui"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$1" == "unstack" ]]; then
|
||||
# Shut down searchlight-ui dashboard services
|
||||
:
|
||||
fi
|
||||
|
||||
if [[ "$1" == "clean" ]]; then
|
||||
# Remove state and transient data
|
||||
# Remember clean.sh first calls unstack.sh
|
||||
|
||||
# Remove searhclight-ui enabled file and pyc
|
||||
rm -f ${SEARCHLIGHT_UI_DIR_ENABLE_FILE}*
|
||||
fi
|
|
@ -0,0 +1,3 @@
|
|||
SEARCHLIGHT_UI_DIR=$DEST/searchlight-ui
|
||||
|
||||
SEARCHLIGHT_UI_DIR_ENABLE_FILE=SEARCHLIGHT_UI_DIR/searchlight_ui/enabled/_1001_project_search_panel.py
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
#'sphinx.ext.intersphinx',
|
||||
'oslosphinx'
|
||||
]
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'searchlight-ui'
|
||||
copyright = u'2016, OpenStack Foundation'
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
add_module_names = True
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme_path = ["."]
|
||||
# html_theme = '_theme'
|
||||
# html_static_path = ['static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index',
|
||||
'%s.tex' % project,
|
||||
u'%s Documentation' % project,
|
||||
u'OpenStack Foundation', 'manual'),
|
||||
]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
|
@ -0,0 +1,4 @@
|
|||
============
|
||||
Contributing
|
||||
============
|
||||
.. include:: ../../CONTRIBUTING.rst
|
|
@ -0,0 +1,35 @@
|
|||
..
|
||||
Copyright 2016, Hewlett-Packard Development Company, L.P.
|
||||
All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Welcome to searchlight-ui's documentation!
|
||||
========================================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
readme
|
||||
installation
|
||||
contributing
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
1. Package the searchlight_ui by running::
|
||||
|
||||
python setup.py sdist
|
||||
|
||||
This will create a python egg in the dist folder, which can be used to
|
||||
install on the horizon machine or within horizon's python virtual
|
||||
environment.
|
||||
|
||||
2. Copy ``_1001_project_search_panel.py`` in
|
||||
``searchlight_ui/enabled`` directory
|
||||
to ``openstack_dashboard/local/enabled``.
|
||||
|
||||
3. (Optional) Copy the policy file into horizon's policy files folder, and
|
||||
add this config ``POLICY_FILES``::
|
||||
|
||||
'searchlight_ui': 'searchlight_ui',
|
||||
|
||||
4. Django has a compressor feature that performs many enhancements for the
|
||||
delivery of static files. If the compressor feature is enabled in your
|
||||
environment (``COMPRESS_OFFLINE = True``), run the following commands::
|
||||
|
||||
$ ./manage.py collectstatic
|
||||
$ ./manage.py compress
|
||||
|
||||
5. Finally restart your web server to enable searchlight-ui
|
||||
in your Horizon::
|
||||
|
||||
$ sudo service apache2 restart
|
|
@ -0,0 +1 @@
|
|||
.. include:: ../../README.rst
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.core.management import execute_from_command_line # noqa
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
|
||||
"openstack_dashboard.settings")
|
||||
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,6 @@
|
|||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from oslo-incubator.git
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=searchlight_ui
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"name": "Searchlight-UI",
|
||||
"description": "Searchlight UI",
|
||||
"repository": "none",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
"eslint": "1.2.1",
|
||||
"eslint-config-openstack": "1.2.3",
|
||||
"eslint-plugin-angular": "0.15.0",
|
||||
"jasmine-core": "2.2.0",
|
||||
"karma": "0.12.31",
|
||||
"karma-chrome-launcher": "0.1.8",
|
||||
"karma-cli": "0.0.4",
|
||||
"karma-coverage": "0.3.1",
|
||||
"karma-jasmine": "0.3.5",
|
||||
"karma-ng-html2js-preprocessor": "0.1.2",
|
||||
"karma-phantomjs-launcher": "0.2.0",
|
||||
"karma-threshold-reporter": "0.1.15",
|
||||
"phantomjs": "1.9.17"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "karma start searchlight_ui/karma.conf.js --single-run",
|
||||
"lint": "eslint --no-color searchlight_ui/static"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
features:
|
||||
- A unified cross resource search panel for the Mitaka release
|
|
@ -0,0 +1,7 @@
|
|||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=1.6 # Apache-2.0
|
||||
Babel>=1.3 # BSD
|
||||
python-barbicanclient>=3.3.0 # Apache-2.0
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pbr.version
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'searchlight_ui').version_string()
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2016, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""This package holds the REST API that supports the LBaaS v2 dashboard
|
||||
Javascript code.
|
||||
|
||||
It is not intended to be used outside of Horizon, and makes no promises of
|
||||
stability or fitness for purpose outside of that scope.
|
||||
|
||||
It does not promise to adhere to the general OpenStack API Guidelines set out
|
||||
in https://wiki.openstack.org/wiki/APIChangeGuidelines.
|
||||
"""
|
||||
|
||||
# import REST API modules here
|
||||
from searchlight_ui.api.rest import searchlight # noqa
|
|
@ -0,0 +1,169 @@
|
|||
# Copyright 2016, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.views import generic
|
||||
import functools
|
||||
import json
|
||||
import requests
|
||||
|
||||
from horizon import exceptions
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.api.rest import urls
|
||||
from openstack_dashboard.api.rest import utils as rest_utils
|
||||
|
||||
|
||||
@urls.register
|
||||
class Search(generic.View):
|
||||
"""Pass-through API for executing searches against searchlight.
|
||||
Horizon only adds auth and CORS proxying.
|
||||
"""
|
||||
url_regex = r'searchlight/search/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def post(self, request):
|
||||
"""Executes a search query against searchlight and returns the 'hits'
|
||||
from the response. Currently accepted parameters are (all optional):
|
||||
|
||||
:param query: see Elasticsearch DSL or Searchlight documentation;
|
||||
defaults to match everything
|
||||
:param index: one or more indices to search. Typically not necessary;
|
||||
prefer using `type` instead
|
||||
:param type: one or more types to search. Uniquely identifies resource
|
||||
types. Example: OS::Glance::Image
|
||||
:param offset: skip over this many results
|
||||
:param limit: return this many results
|
||||
:param sort: sort by one or more fields
|
||||
:param fields: restrict the fields returned for each document
|
||||
:param highlight: add an Elasticsearch highlight clause
|
||||
"""
|
||||
search_parameters = dict(request.DATA) if request.DATA else {}
|
||||
|
||||
# Set some defaults
|
||||
search_parameters.setdefault('limit', 20)
|
||||
search_parameters.setdefault('query', {'match_all': {}})
|
||||
|
||||
# Example:
|
||||
# {"hits": ["_id": abc, "_source": {..}], "max_score": 2.0, "total": 3}
|
||||
return searchlight_post(
|
||||
'/search',
|
||||
request,
|
||||
search_parameters
|
||||
).json()['hits']
|
||||
|
||||
|
||||
@urls.register
|
||||
class Plugins(generic.View):
|
||||
"""API call to interrogate searchlight for enabled resource types.
|
||||
Use to determine the types you can query.
|
||||
"""
|
||||
url_regex = r'searchlight/plugins/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""Requests enabled searchlight plugins.
|
||||
At this time the response looks like:
|
||||
{"plugins": [{
|
||||
"index": "searchlight",
|
||||
"type": "OS::Glance::Image",
|
||||
"name": "OS::Glance::Image"
|
||||
}.. ]
|
||||
"""
|
||||
return searchlight_get('/search/plugins', request).json()
|
||||
|
||||
|
||||
@urls.register
|
||||
class Facets(generic.View):
|
||||
"""API call to interrogate searchlight for available search facets."""
|
||||
url_regex = r'searchlight/facets/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""Requests available facets for the different resource types.
|
||||
|
||||
:param type: optional type to limit facets returned.
|
||||
Uniquely identifies resource types.
|
||||
Example: OS::Glance::Image
|
||||
:param index: optional search index to limit facets returned.
|
||||
Typically not needed, using the type will
|
||||
automatically map to the index unless deployer
|
||||
has changes.
|
||||
At this time the response looks like:
|
||||
{
|
||||
"OS::Glance::Image": [
|
||||
{
|
||||
"name": "status",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"type": "date"
|
||||
}
|
||||
...
|
||||
],
|
||||
"OS::Nova::Server": [
|
||||
{
|
||||
"name": "status",
|
||||
"options": [
|
||||
{
|
||||
"doc_count": 1,
|
||||
"key": "ACTIVE"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Set some defaults
|
||||
return searchlight_get('/search/facets',
|
||||
request,
|
||||
params=request.GET).json()
|
||||
|
||||
|
||||
def _searchlight_request(request_method, url, request, data=None, params=None):
|
||||
"""Makes a request to searchlight with an optional payload. Should set
|
||||
any necessary auth headers and SSL parameters.
|
||||
"""
|
||||
# Set verify if a CACERT is set and SSL_NO_VERIFY isn't True
|
||||
verify = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
|
||||
if getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False):
|
||||
verify = False
|
||||
|
||||
return request_method(
|
||||
_get_searchlight_url(request) + url,
|
||||
headers={'X-Auth-Token': request.user.token.id},
|
||||
data=json.dumps(data) if data else None,
|
||||
verify=verify,
|
||||
params=params
|
||||
)
|
||||
|
||||
|
||||
# Create some convenience partial functions
|
||||
searchlight_get = functools.partial(_searchlight_request, requests.get)
|
||||
searchlight_post = functools.partial(_searchlight_request, requests.post)
|
||||
|
||||
|
||||
def _get_searchlight_url(request):
|
||||
"""Get searchlight's URL from keystone; allow an override in settings"""
|
||||
searchlight_url = getattr(settings, 'SEARCHLIGHT_URL', None)
|
||||
try:
|
||||
searchlight_url = base.url_for(request, 'search')
|
||||
except exceptions.ServiceCatalogException:
|
||||
pass
|
||||
# Currently the keystone endpoint is http://host:port/
|
||||
# without the version.
|
||||
return searchlight_url + 'v1'
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2016, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""This package holds the REST API that supports the LBaaS v2 dashboard
|
||||
Javascript code.
|
||||
|
||||
It is not intended to be used outside of Horizon, and makes no promises of
|
||||
stability or fitness for purpose outside of that scope.
|
||||
|
||||
It does not promise to adhere to the general OpenStack API Guidelines set out
|
||||
in https://wiki.openstack.org/wiki/APIChangeGuidelines.
|
||||
"""
|
||||
|
||||
# import REST API modules here
|
||||
from searchlight_ui.api.rest import searchlight # noqa
|
|
@ -0,0 +1,23 @@
|
|||
# (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class Search(horizon.Panel):
|
||||
name = _("Search")
|
||||
slug = 'search'
|
||||
permissions = ('openstack.services.search',)
|
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Search" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<hz-page-header header="{% trans "Search" %}"></hz-page-header>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<ng-include src="'{{ STATIC_URL }}dashboard/project/search/table/search-table.html'"></ng-include>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
# (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.search import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'openstack_dashboard.dashboards.project.search.views',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
# (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.views import generic
|
||||
|
||||
|
||||
class IndexView(generic.TemplateView):
|
||||
template_name = 'project/search/index.html'
|
|
@ -0,0 +1,38 @@
|
|||
# (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# The slug of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'project'
|
||||
|
||||
# The slug of the panel group the PANEL is associated with.
|
||||
# If you want the panel to show up without a panel group,
|
||||
# use the panel group "default".
|
||||
PANEL_GROUP = 'default'
|
||||
|
||||
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'search'
|
||||
|
||||
# If set to True, this settings file will not be added to the settings.
|
||||
DISABLED = False
|
||||
|
||||
ADD_INSTALLED_APPS = ['searchlight_ui']
|
||||
|
||||
# Python panel class of the PANEL to be added.
|
||||
ADD_PANEL = 'searchlight_ui.dashboards.project.search.panel.Search'
|
||||
|
||||
ADD_ANGULAR_MODULES = ['horizon.dashboard.project.search']
|
||||
|
||||
ADD_SCSS_FILES = ['dashboard/project/search/search.scss']
|
||||
|
||||
AUTO_DISCOVER_STATIC_FILES = True
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2015 IBM Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
module.exports = function (config) {
|
||||
var xstaticPath;
|
||||
var basePaths = [
|
||||
'../horizon/.venv'
|
||||
];
|
||||
|
||||
for (var i = 0; i < basePaths.length; i++) {
|
||||
var basePath = path.resolve(basePaths[i]);
|
||||
|
||||
if (fs.existsSync(basePath)) {
|
||||
xstaticPath = basePath + '/lib/python2.7/site-packages/xstatic/pkg/';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!xstaticPath) {
|
||||
console.error('xStatic libraries not found, please set up venv');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.set({
|
||||
preprocessors: {
|
||||
// Used to collect templates for preprocessing.
|
||||
// NOTE: the templates must also be listed in the files section below.
|
||||
'./static/**/*.html': ['ng-html2js'],
|
||||
// Used to indicate files requiring coverage reports.
|
||||
'./static/**/!(*.spec).js': ['coverage'],
|
||||
},
|
||||
|
||||
// Sets up module to process templates.
|
||||
ngHtml2JsPreprocessor: {
|
||||
prependPrefix: '/',
|
||||
moduleName: 'templates'
|
||||
},
|
||||
|
||||
// Assumes you're in the top-level horizon directory.
|
||||
basePath: './',
|
||||
|
||||
// Contains both source and test files.
|
||||
files: [
|
||||
/*
|
||||
* shim, partly stolen from /i18n/js/horizon/
|
||||
* Contains expected items not provided elsewhere (dynamically by
|
||||
* Django or via jasmine template.
|
||||
*/
|
||||
'../../horizon/test-shim.js',
|
||||
|
||||
// from jasmine.html
|
||||
xstaticPath + 'jquery/data/jquery.js',
|
||||
xstaticPath + 'angular/data/angular.js',
|
||||
xstaticPath + 'angular/data/angular-route.js',
|
||||
xstaticPath + 'angular/data/angular-mocks.js',
|
||||
xstaticPath + 'angular/data/angular-cookies.js',
|
||||
xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js',
|
||||
xstaticPath + 'angular_gettext/data/angular-gettext.js',
|
||||
xstaticPath + 'angular/data/angular-sanitize.js',
|
||||
xstaticPath + 'd3/data/d3.js',
|
||||
xstaticPath + 'rickshaw/data/rickshaw.js',
|
||||
xstaticPath + 'angular_smart_table/data/smart-table.js',
|
||||
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
|
||||
xstaticPath + 'spin/data/spin.js',
|
||||
xstaticPath + 'spin/data/spin.jquery.js',
|
||||
|
||||
// TODO: These should be mocked.
|
||||
'../../horizon/horizon/static/horizon/js/horizon.js',
|
||||
|
||||
/**
|
||||
* Include framework source code from horizon that we need.
|
||||
* Otherwise, karma will not be able to find them when testing.
|
||||
* These files should be mocked in the foreseeable future.
|
||||
*/
|
||||
'../../horizon/horizon/static/framework/**/*.module.js',
|
||||
'../../horizon/horizon/static/framework/**/!(*.spec|*.mock).js',
|
||||
'../../horizon/openstack_dashboard/static/**/*.module.js',
|
||||
'../../horizon/openstack_dashboard/static/**/!(*.spec|*.mock).js',
|
||||
'../../horizon/openstack_dashboard/dashboards/**/static/**/*.module.js',
|
||||
'../../horizon/openstack_dashboard/dashboards/**/static/**/!(*.spec|*.mock).js',
|
||||
|
||||
/**
|
||||
* First, list all the files that defines application's angular modules.
|
||||
* Those files have extension of `.module.js`. The order among them is
|
||||
* not significant.
|
||||
*/
|
||||
'./static/**/*.module.js',
|
||||
|
||||
/**
|
||||
* Followed by other JavaScript files that defines angular providers
|
||||
* on the modules defined in files listed above. And they are not mock
|
||||
* files or spec files defined below. The order among them is not
|
||||
* significant.
|
||||
*/
|
||||
'./static/**/!(*.spec|*.mock).js',
|
||||
|
||||
/**
|
||||
* Then, list files for mocks with `mock.js` extension. The order
|
||||
* among them should not be significant.
|
||||
*/
|
||||
'../../horizon/openstack_dashboard/static/**/*.mock.js',
|
||||
//'./static/**/*.mock.js',
|
||||
|
||||
/**
|
||||
* Finally, list files for spec with `spec.js` extension. The order
|
||||
* among them should not be significant.
|
||||
*/
|
||||
'./static/**/*.spec.js',
|
||||
|
||||
/**
|
||||
* Angular external templates
|
||||
*/
|
||||
'./static/**/*.html'
|
||||
],
|
||||
|
||||
autoWatch: true,
|
||||
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
browsers: ['PhantomJS'],
|
||||
|
||||
phantomjsLauncher: {
|
||||
// Have phantomjs exit if a ResourceError is encountered
|
||||
// (useful if karma exits without killing phantom)
|
||||
exitOnResourceError: true
|
||||
},
|
||||
|
||||
reporters: ['progress', 'coverage', 'threshold'],
|
||||
|
||||
plugins: [
|
||||
'karma-phantomjs-launcher',
|
||||
'karma-jasmine',
|
||||
'karma-ng-html2js-preprocessor',
|
||||
'karma-coverage',
|
||||
'karma-threshold-reporter'
|
||||
],
|
||||
|
||||
coverageReporter: {
|
||||
type: 'html',
|
||||
dir: '../coverage-karma/'
|
||||
},
|
||||
|
||||
// Coverage threshold values.
|
||||
thresholdReporter: {
|
||||
statements: 1,
|
||||
branches: 1,
|
||||
functions: 1,
|
||||
lines: 1
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Copyright 2016, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.openstack-service-api')
|
||||
.factory('horizon.app.core.openstack-service-api.searchlight', SearchlightAPI);
|
||||
|
||||
SearchlightAPI.$inject = [
|
||||
'horizon.framework.util.http.service',
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.app.core.openstack-service-api.searchlight
|
||||
* @description Provides direct access to Searchlight APIs.
|
||||
*/
|
||||
function SearchlightAPI(apiService, toastService) {
|
||||
|
||||
var service = {
|
||||
postSearch: postSearch,
|
||||
getPlugins: getPlugins,
|
||||
getFacets: getFacets
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.searchlight.postSearch
|
||||
* @description
|
||||
* Runs a search.
|
||||
*
|
||||
* The return value will be an object with keys 'total', 'max_score',
|
||||
* 'hits'. 'hits' is a list containing objects which are results from
|
||||
* elasticsearch. Each result is an object with keys '_id', 'index', 'type'
|
||||
* and '_source', the latter being the document source. See the searchlight
|
||||
* documentation for a full list of options.
|
||||
*
|
||||
* @param {Object} queryParams
|
||||
* Query parameters. Optional.
|
||||
*
|
||||
* @param {Object} queryParams.query
|
||||
* Search filter. The default is {"match_all": {}}. See the Elasticsearch
|
||||
* query DSL (https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html).
|
||||
*
|
||||
* @param {string} queryParams.type
|
||||
* Limit results to one or more types (e.g. OS::Glance::Image).
|
||||
*
|
||||
* @param {Object} queryParams.sort
|
||||
* Set sort order; can be a string (leading '-' for descending order'),
|
||||
* object ({"field_name": {"order": "asc"}}) or a list of objects and/or
|
||||
* strings for multiple sort fields.
|
||||
*
|
||||
* @param {number} queryParams.limit
|
||||
* Limit on the number of results.
|
||||
*
|
||||
* @param {number} queryParams.offset
|
||||
* Offset for search results
|
||||
*/
|
||||
function postSearch(queryParams, suppressToast) {
|
||||
queryParams = (queryParams) ? queryParams : {};
|
||||
|
||||
var promise = apiService.post('/api/searchlight/search/', queryParams);
|
||||
|
||||
return suppressToast ? promise : promise.error(function () {
|
||||
toastService.add('error', gettext('Unable to execute the search.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.searchlight.getPlugins
|
||||
* @description
|
||||
* Get a list of enabled searchlight resources.
|
||||
*
|
||||
* {"plugins": [{"name": "OS::Glance::Image",
|
||||
* "index": "glance", "type": "OS::Glance::Image"}
|
||||
* .. ]
|
||||
*/
|
||||
function getPlugins() {
|
||||
return apiService.get('/api/searchlight/plugins/')
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to retrieve Searchlight resources types.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.searchlight.getFacets
|
||||
* @description
|
||||
* Get a list of search resource facets
|
||||
*
|
||||
* @param {Object} params
|
||||
* Query parameters. Optional.
|
||||
*
|
||||
* @param {Object} params.index_name
|
||||
* The index name to get results from.
|
||||
*
|
||||
* @param {string} params.doc_type
|
||||
* The document type. eg. OS::Nova::Server
|
||||
*
|
||||
* @param {boolean} suppressToast
|
||||
* If passed in, this will not show the default error handling
|
||||
* (horizon alert). The glance API may not have metadata definitions
|
||||
* enabled.
|
||||
*/
|
||||
function getFacets(params, suppressToast) {
|
||||
var config = (params) ? {'params': params} : {};
|
||||
var promise = apiService.get('/api/searchlight/facets/', config);
|
||||
|
||||
return suppressToast ? promise : promise.error(function() {
|
||||
toastService.add('error', gettext('Unable to retrieve search facets.'));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('Searchlight API', function () {
|
||||
var testCall, service;
|
||||
var apiService = {};
|
||||
var toastService = {};
|
||||
|
||||
beforeEach(
|
||||
module('horizon.mock.openstack-service-api',
|
||||
function ($provide, initServices) {
|
||||
testCall = initServices($provide, apiService, toastService);
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
|
||||
beforeEach(
|
||||
inject(['horizon.app.core.openstack-service-api.searchlight', function (searchlightAPI) {
|
||||
service = searchlightAPI;
|
||||
}])
|
||||
);
|
||||
|
||||
it('defines the service', function () {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
var tests = [
|
||||
{
|
||||
func: 'postSearch',
|
||||
method: 'post',
|
||||
path: '/api/searchlight/search/',
|
||||
data: {
|
||||
"query": {"match_all": {}},
|
||||
"limit": 10,
|
||||
"offset": 20
|
||||
},
|
||||
error: 'Unable to execute the search.',
|
||||
testInput: [{
|
||||
"query": {"match_all": {}},
|
||||
"limit": 10,
|
||||
"offset": 20
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
// Iterate through the defined tests and apply as Jasmine specs.
|
||||
angular.forEach(tests, function (params) {
|
||||
it('defines the ' + params.func + ' call properly', function () {
|
||||
var callParams = [apiService, service, toastService, params];
|
||||
testCall.apply(this, callParams);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.filter('commonStatus', commonStatusFilter);
|
||||
|
||||
commonStatusFilter.$inject = [
|
||||
'$filter',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name commonStatusFilter
|
||||
* @description
|
||||
* Takes raw status from the API and returns the user friendly status if found.
|
||||
*
|
||||
* @param {function} $filter angular filter
|
||||
*
|
||||
* @param {function} gettext internationalization
|
||||
*
|
||||
* @returns {String} User friendly status if found.
|
||||
*/
|
||||
function commonStatusFilter($filter, gettext) {
|
||||
var commonStatuses = {
|
||||
'active': gettext('Active'),
|
||||
'down': gettext('Down'),
|
||||
'saving': gettext('Saving'),
|
||||
'queued': gettext('Queued'),
|
||||
'pending_delete': gettext('Pending Delete'),
|
||||
'pending': gettext('Pending'),
|
||||
'killed': gettext('Killed'),
|
||||
'deleted': gettext('Deleted'),
|
||||
'shutoff': gettext('Shutoff'),
|
||||
'suspended': gettext('Suspended'),
|
||||
'paused': gettext('Paused'),
|
||||
'error': gettext('Error'),
|
||||
'rescue': gettext('Rescue'),
|
||||
'shelved': gettext('Shelved'),
|
||||
'shelved_offloaded': gettext('Shelved Offloaded')
|
||||
};
|
||||
|
||||
return function findStatus(input) {
|
||||
if (angular.isDefined(input)) {
|
||||
input = $filter('lowercase')(input);
|
||||
}
|
||||
var result = commonStatuses[input];
|
||||
return angular.isDefined(result) ? result : input;
|
||||
};
|
||||
}
|
||||
|
||||
}());
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.search.commonStatus Filter', function () {
|
||||
beforeEach(module('horizon.framework.util.i18n'));
|
||||
beforeEach(module('horizon.dashboard.project.search'));
|
||||
|
||||
describe('commonStatus', function () {
|
||||
var commonStatusFilter;
|
||||
beforeEach(inject(function (_commonStatusFilter_) {
|
||||
commonStatusFilter = _commonStatusFilter_;
|
||||
}));
|
||||
|
||||
it('Returns value when key is present', function () {
|
||||
expect(commonStatusFilter('active')).toBe('Active');
|
||||
});
|
||||
|
||||
it('Returns input when key is not present', function () {
|
||||
expect(commonStatusFilter('unknown')).toBe('unknown');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.filter('searchPluginResourceTypes', searchPluginResourceTypes);
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name pluginTypes
|
||||
* @description
|
||||
* Filters the available search resource types.
|
||||
*
|
||||
* @returns {String} Plugin types.
|
||||
*/
|
||||
function searchPluginResourceTypes() {
|
||||
return function plugins(pluginz, options) {
|
||||
options = options || {};
|
||||
var excludedTypes = options.excludedTypes || [];
|
||||
var includedTypes = options.includedTypes || [];
|
||||
var flatten = options.flatten || false;
|
||||
var result = [];
|
||||
|
||||
angular.forEach(pluginz, filterAndMapPlugin);
|
||||
|
||||
function filterAndMapPlugin(plugin) {
|
||||
if (excludedTypes.indexOf(plugin.type) >= 0) {
|
||||
return;
|
||||
} else if (includedTypes.length === 0 || includedTypes.indexOf(plugin.type) >= 0) {
|
||||
result.push(flatten ? plugin.type : plugin);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.filter('resourceLabeler', resourceLabelerFilter);
|
||||
|
||||
resourceLabelerFilter.$inject = [
|
||||
'titleFilter',
|
||||
'noUnderscoreFilter',
|
||||
'horizon.framework.conf.resource-type-registry.service',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name resourceTypeLabelerFilter
|
||||
* @description
|
||||
* Takes raw status from the API and returns the user friendly status if found.
|
||||
*
|
||||
* @param {function} titleFilter Horizon title filter
|
||||
*
|
||||
* @param {function} noUnderscoreFilter Horizon noUnderscoreFilter
|
||||
*
|
||||
* @param {function} registry resource type registry
|
||||
*
|
||||
* @param {function} gettext internationalization
|
||||
*
|
||||
* @returns {String} User friendly status if found.
|
||||
*/
|
||||
function resourceLabelerFilter(titleFilter, noUnderscoreFilter, registry, gettext) {
|
||||
|
||||
return function label(resourceType, input, propertyName) {
|
||||
var resourceTypeRegistration = registry.getResourceType(resourceType);
|
||||
|
||||
var output = input;
|
||||
|
||||
if (angular.isDefined(resourceTypeRegistration)) {
|
||||
if (angular.isUndefined(input)) {
|
||||
output = resourceTypeRegistration.getName(1) || resourceType;
|
||||
} else if (angular.isUndefined(propertyName)) {
|
||||
output = resourceTypeRegistration.label(input);
|
||||
} else {
|
||||
output = resourceTypeRegistration.format(propertyName, input);
|
||||
}
|
||||
}
|
||||
|
||||
if (output === input) {
|
||||
output = input;
|
||||
// There was no registered label. Let's Try to make it look human.
|
||||
|
||||
// Extensions
|
||||
var osExtRegEx = new RegExp('OS-EXT-.*:', 'i');
|
||||
output = angular.isString(output)
|
||||
? output.replace(osExtRegEx, gettext('(Extension)') + ' ')
|
||||
: output;
|
||||
|
||||
output = titleFilter(noUnderscoreFilter(output));
|
||||
|
||||
var idRegEx = new RegExp('id', 'ig');
|
||||
output = angular.isString(output)
|
||||
? output.replace(idRegEx, 'ID') : output;
|
||||
|
||||
// Swift - could go away with default value function registration
|
||||
var xObjectRegEx = new RegExp('x-object-');
|
||||
output = angular.isString(output)
|
||||
? output.replace(xObjectRegEx, '(Custom)') : output;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.filter('resourceUrl', resourceUrlFilter);
|
||||
|
||||
resourceUrlFilter.$inject = [
|
||||
'horizon.dashboard.project.search.resourceLocator'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name resourceUrlFilter
|
||||
* @description
|
||||
* Takes a resource search hit from Searchlight and finds the URL for it.
|
||||
*
|
||||
* @param {function} resourceLocator service with URLs
|
||||
*
|
||||
* @return {String} URL for the requested URL.
|
||||
*/
|
||||
function resourceUrlFilter(resourceLocator) {
|
||||
return function hitMapper(hit, options) {
|
||||
return resourceLocator.getResourceUrl(hit, options);
|
||||
};
|
||||
}
|
||||
|
||||
}());
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @ngname horizon.dashboard.project.search
|
||||
*
|
||||
* @description
|
||||
* Provides the services and widgets required
|
||||
* to support and display the project search panel.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.search', [])
|
||||
.config(config);
|
||||
|
||||
config.$inject = [
|
||||
'$provide',
|
||||
'$windowProvider'
|
||||
];
|
||||
|
||||
/**
|
||||
* @name horizon.dashboard.project.search.basePath
|
||||
* @description Base path for the project dashboard
|
||||
*
|
||||
* @param {function} $provide ng provide service
|
||||
*
|
||||
* @param {function} $windowProvider NG window provider
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function config($provide, $windowProvider) {
|
||||
var path = $windowProvider.$get().STATIC_URL + 'dashboard/project/search/';
|
||||
$provide.constant('horizon.dashboard.project.search.basePath', path);
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.search', function() {
|
||||
it('should exist', function() {
|
||||
expect(angular.module('horizon.dashboard.project.search')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizon.dashboard.project.search.basePath constant', function () {
|
||||
var searchBasePath, staticUrl;
|
||||
|
||||
beforeEach(module('horizon.dashboard.project.search'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
searchBasePath = $injector.get('horizon.dashboard.project.search.basePath');
|
||||
staticUrl = $injector.get('$window').STATIC_URL;
|
||||
}));
|
||||
|
||||
it('should be defined', function () {
|
||||
expect(searchBasePath).toBeDefined();
|
||||
});
|
||||
|
||||
it('should equal to "/static/dashboard/project/search/"', function () {
|
||||
expect(searchBasePath).toEqual(staticUrl + 'dashboard/project/search/');
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
|
@ -0,0 +1,8 @@
|
|||
table[ng-controller="searchTableController as table"] {
|
||||
|
||||
.detail-expanded .row {
|
||||
background: none;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name horizon.dashboard.project.search.settingsController
|
||||
*
|
||||
* @param(object) modal instance from angular-bootstrap
|
||||
* @param(object) the settings to display
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.controller('searchSettingsController', SettingsController);
|
||||
|
||||
SettingsController.$inject = [
|
||||
'$modalInstance',
|
||||
'searchSettings'
|
||||
];
|
||||
|
||||
function SettingsController($modalInstance, searchSettings) {
|
||||
var ctrl = this;
|
||||
ctrl.settings = searchSettings;
|
||||
ctrl.dismiss = dismiss;
|
||||
ctrl.apply = apply;
|
||||
|
||||
function apply() {
|
||||
$modalInstance.close();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
$modalInstance.dismiss('cancel');
|
||||
}
|
||||
|
||||
} // end of function
|
||||
|
||||
})();
|
|
@ -0,0 +1,99 @@
|
|||
<div ng-form="searchSettingsForm">
|
||||
<div class="modal-header ui-draggable-handle">
|
||||
<a href="#" class="close" ng-click="$dismiss()">
|
||||
<span class="fa fa-times"></span>
|
||||
</a>
|
||||
<div class="h3 modal-title" translate>Search Settings</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<fieldset>
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error': searchSettingsForm.name.$invalid && searchSettingsForm.name.$dirty}">
|
||||
<div>
|
||||
<label class="required" for="id_resultLimit">
|
||||
<span translate>Result Limit</span>
|
||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||
<input class="form-control"
|
||||
id="id_resultLimit"
|
||||
name="resultLimit"
|
||||
ng-model="ctrl.settings.general.limit"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1000"
|
||||
placeholder="{$ '1 to 1000' | translate $}">
|
||||
</label>
|
||||
</div>
|
||||
<span class="help-block"
|
||||
ng-show="searchSettingsForm.resultLimit.$error.max"
|
||||
translate>
|
||||
Do not exceed 1000. Recommended is less than 100.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<label for="id_pollEnabled">
|
||||
<span translate>Poll for Updates</span>
|
||||
<input type="checkbox"
|
||||
ng-model="ctrl.settings.polling.enabled"
|
||||
name="pollEnabled"
|
||||
checked id="id_pollEnabled">
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_pollInterval">
|
||||
<span translate>Update Interval (milliseconds)</span>
|
||||
<input class="form-control"
|
||||
id="id_pollInterval"
|
||||
name="pollInterval"
|
||||
ng-disabled="!ctrl.settings.polling.enabled"
|
||||
ng-model="ctrl.settings.polling.interval"
|
||||
type="number"
|
||||
min="1000"
|
||||
max="60000"
|
||||
placeholder="{$ '1000 to 60000' | translate $}"
|
||||
>
|
||||
</label>
|
||||
<span class="help-block"
|
||||
ng-show="searchSettingsForm.pollInterval.$error.max"
|
||||
translate>
|
||||
Do not exceed 60000.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<p translate>
|
||||
These settings impact the processing of results returned
|
||||
from the search service. Changing these settings may
|
||||
impact search performance and results.
|
||||
</p>
|
||||
<p translate>
|
||||
It is typically better to filter results further than to increase
|
||||
the number of returned results and page through them.
|
||||
</p>
|
||||
<p translate>
|
||||
Polling enables you to create a view of the data the updates
|
||||
periodically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<!-- button class="btn btn-default secondary" ng-click="$dismiss()">
|
||||
<span class="fa fa-close"></span>
|
||||
<translate>Cancel</translate>
|
||||
</button -->
|
||||
<button class="btn btn-primary"
|
||||
ng-click="$close(ctrl.model)"
|
||||
ng-disabled="searchSettingsForm.$invalid">
|
||||
<span class="fa fa-refresh"></span>
|
||||
<translate>Update Search</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.settingsService', searchSettingsService);
|
||||
|
||||
searchSettingsService.$inject = [
|
||||
'$modal',
|
||||
'horizon.dashboard.project.search.basePath',
|
||||
'horizon.app.core.openstack-service-api.searchlight'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.dashboard.project.search.settingsService
|
||||
*
|
||||
* @Description
|
||||
* Provides general search settings and a modal for updating the settings.
|
||||
*
|
||||
* @param {function} $modal ng $modal service
|
||||
*
|
||||
* @param {function} basePath the base url path
|
||||
*
|
||||
* @param {function} searchlight searchlight API service
|
||||
*
|
||||
* @returns {function} This settings service.
|
||||
*/
|
||||
function searchSettingsService($modal,
|
||||
basePath,
|
||||
searchlight)
|
||||
{
|
||||
var service = {
|
||||
events: {
|
||||
settingsUpdatedEvent: 'horizon.dashboard.project.search.settingsUpdated',
|
||||
pluginsUpdatedEvent: 'horizon.dashboard.project.search.pluginsUpdated'
|
||||
},
|
||||
open: open,
|
||||
initScope: initScope,
|
||||
initPlugin: initPlugins,
|
||||
settings: {
|
||||
availablePlugins: [],
|
||||
fullTextSearch: {
|
||||
delayInMS: 400,
|
||||
phrase_slop: 10,
|
||||
lenient: true,
|
||||
analyze_wildCard: true
|
||||
},
|
||||
general: {
|
||||
limit: 50
|
||||
},
|
||||
polling: {
|
||||
enabled: false,
|
||||
interval: 10000 //Milliseconds
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//init();
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
function init() {
|
||||
initPlugins();
|
||||
}
|
||||
|
||||
function initPlugins() {
|
||||
searchlight.getPlugins().success(pluginsReceived);
|
||||
|
||||
function pluginsReceived(response) {
|
||||
service.settings.availablePlugins = response.plugins;
|
||||
scope.$emit(service.events.pluginsUpdatedEvent, response.plugins);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO add subscribe instead of this.
|
||||
|
||||
var scope;
|
||||
|
||||
function initScope(newScope) {
|
||||
if (scope !== newScope) {
|
||||
scope = newScope;
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
function getSearchSettings() {
|
||||
return service.settings;
|
||||
}
|
||||
|
||||
var resolve = {
|
||||
searchSettings: getSearchSettings
|
||||
};
|
||||
|
||||
var options = {
|
||||
controller: 'searchSettingsController as ctrl',
|
||||
scope: scope,
|
||||
backdrop: 'static',
|
||||
templateUrl: basePath + 'settings/search-settings.html',
|
||||
resolve: resolve
|
||||
};
|
||||
|
||||
$modal.open(options)
|
||||
.result
|
||||
.then(notifySettingsUpdated);
|
||||
|
||||
function notifySettingsUpdated() {
|
||||
scope.$emit(service.events.settingsUpdatedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.search.settingsService', function() {
|
||||
|
||||
var service, $scope;
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
beforeEach(module('horizon.dashboard.project.search'));
|
||||
|
||||
beforeEach(inject(function($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
service = $injector.get('horizon.dashboard.project.search.settingsService');
|
||||
}));
|
||||
|
||||
it('should open the modal', function() {
|
||||
service.initScope($scope);
|
||||
service.open({resultLimit: 1});
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.directive('hzArrayFieldTable', hzArrayFieldTable);
|
||||
|
||||
hzArrayFieldTable.$inject = [
|
||||
'horizon.dashboard.project.search.basePath'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hzSearchHighlighter
|
||||
* @description
|
||||
* Takes a searchlight "hit" (search result) and if the requested "field" is highlighted
|
||||
* in the results, outputs the highlighted result. Otherwise, outputs the "default-falue".
|
||||
*
|
||||
* @param {function} basePath the base url path
|
||||
*
|
||||
* @returns {function} This directive.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <hz-array-field-table hit="hit"
|
||||
* field="'name'"
|
||||
* default-value="hit._source.name || hit._source._id | noValue">
|
||||
* </hz-search-highlighter>
|
||||
*/
|
||||
function hzArrayFieldTable(basePath) {
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
rawArray: '=array'
|
||||
},
|
||||
templateUrl: basePath + 'table/hz-array-field-table.html'
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="(key, val) in rawArray[0]">{{ key }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="item in rawArray">
|
||||
<td ng-repeat="(key, val) in item">{{ val }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name SearchTableController
|
||||
*
|
||||
* @description
|
||||
* Controller for the search table.
|
||||
* Serves as the focal point for table actions.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.controller('searchTableController', SearchTableController);
|
||||
|
||||
SearchTableController.$inject = [
|
||||
'$scope',
|
||||
'$filter',
|
||||
'$timeout',
|
||||
'searchPluginResourceTypesFilter',
|
||||
'horizon.framework.conf.resource-type-registry.service',
|
||||
'horizon.dashboard.project.search.searchlightFacetUtils',
|
||||
'horizon.dashboard.project.search.searchlightSearchHelper',
|
||||
'horizon.dashboard.project.search.resourceLocator',
|
||||
'horizon.dashboard.project.search.settingsService'
|
||||
];
|
||||
|
||||
function SearchTableController($scope,
|
||||
$filter,
|
||||
$timeout,
|
||||
searchPluginResourceTypesFilter,
|
||||
registry,
|
||||
searchlightFacetUtils,
|
||||
searchlightSearchHelper,
|
||||
resourceLocator,
|
||||
searchSettings)
|
||||
{
|
||||
var ctrl = this;
|
||||
ctrl.filter = $filter;
|
||||
ctrl.hits = [];
|
||||
ctrl.hitsSrc = [];
|
||||
ctrl.initialized = false;
|
||||
ctrl.isNested = function(input) {
|
||||
var result = angular.isArray(input) &&
|
||||
input.length > 0 &&
|
||||
angular.isObject(input[0]) &&
|
||||
Object.keys(input[0]).length > 1;
|
||||
|
||||
return result;
|
||||
};
|
||||
ctrl.resourceLocator = resourceLocator;
|
||||
ctrl.searchFacets = [];
|
||||
ctrl.excludedTypes = ['OS::Glance::Metadef'];
|
||||
ctrl.searchSettings = searchSettings;
|
||||
ctrl.defaultResourceTypes = [];
|
||||
ctrl.defaultFacets = searchlightFacetUtils.defaultFacets();
|
||||
ctrl.registry = registry;
|
||||
ctrl.actionResultHandler = actionResultHandler;
|
||||
|
||||
init();
|
||||
|
||||
////////////////////////////////
|
||||
|
||||
function init() {
|
||||
ctrl.searchSettings.initScope($scope);
|
||||
searchlightFacetUtils.initScope($scope);
|
||||
|
||||
if (searchlightSearchHelper.lastSearchQueryOptions) {
|
||||
ctrl.searchFacets = searchlightSearchHelper.lastSearchQueryOptions.searchFacets;
|
||||
} else {
|
||||
ctrl.searchFacets = ctrl.defaultFacets;
|
||||
}
|
||||
}
|
||||
|
||||
var pluginsUpdatedWatcher = $scope.$on(
|
||||
ctrl.searchSettings.events.pluginsUpdatedEvent,
|
||||
pluginsUpdated
|
||||
);
|
||||
|
||||
function pluginsUpdated(event, plugins) {
|
||||
var pluginToTypesOptions = {
|
||||
excludedTypes: ctrl.excludedTypes,
|
||||
flatten: true
|
||||
};
|
||||
ctrl.defaultResourceTypes = searchPluginResourceTypesFilter(plugins, pluginToTypesOptions);
|
||||
|
||||
ctrl.defaultResourceTypes.forEach(function(type) {
|
||||
registry.initActions(type, $scope);
|
||||
});
|
||||
|
||||
searchlightFacetUtils.setTypeFacetFromResourceTypes(
|
||||
ctrl.defaultResourceTypes, ctrl.searchFacets);
|
||||
|
||||
searchlightFacetUtils.broadcastFacetsChanged(searchlightSearchHelper.lastSearchQueryOptions);
|
||||
|
||||
ctrl.initialized = true;
|
||||
|
||||
if (searchlightSearchHelper.lastSearchQueryOptions) {
|
||||
searchlightSearchHelper.lastSearchQueryOptions.onSearchSuccess = onSearchResult;
|
||||
searchlightSearchHelper.lastSearchQueryOptions.onSearchError = onSearchResult;
|
||||
searchlightSearchHelper.repeatLastSearchWithLatestSettings();
|
||||
} else {
|
||||
search();
|
||||
}
|
||||
}
|
||||
|
||||
var fullTextSearchTimeout;
|
||||
var searchUpdatedWatcher = $scope.$on('serverSearchUpdated', function (event, searchData) {
|
||||
|
||||
// Magic search always broadcasts this at startup, so
|
||||
// we have to not run until we are fully initialized.
|
||||
if (!ctrl.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
fullTextSearchTimeout = null;
|
||||
search(searchData);
|
||||
}
|
||||
|
||||
if (searchData.queryStringChanged) {
|
||||
// This keeps the query from being executed too rapidly
|
||||
// when the user is performing rapid key presses.
|
||||
if (fullTextSearchTimeout) {
|
||||
$timeout.cancel(fullTextSearchTimeout);
|
||||
}
|
||||
|
||||
fullTextSearchTimeout = $timeout(
|
||||
performSearch,
|
||||
ctrl.searchSettings.settings.fullTextSearch.delayInMS
|
||||
);
|
||||
} else if (searchData.magicSearchQueryChanged) {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
var checkFacetsWatcher = $scope.$on('checkFacets', function (event, selectedFacets) {
|
||||
//Facets are actually DOM elements. This affects the styling.
|
||||
$timeout(function () {
|
||||
angular.forEach(selectedFacets, function setIsServerTrue(facet) {
|
||||
facet.isServer = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var searchSettingsUpdatedWatcher = $scope.$on(
|
||||
ctrl.searchSettings.events.settingsUpdatedEvent,
|
||||
searchlightSearchHelper.repeatLastSearchWithLatestSettings
|
||||
);
|
||||
|
||||
$scope.$on('$destroy', function cleanupListeners() {
|
||||
checkFacetsWatcher();
|
||||
searchUpdatedWatcher();
|
||||
searchSettingsUpdatedWatcher();
|
||||
pluginsUpdatedWatcher();
|
||||
});
|
||||
|
||||
function search(queryOptions) {
|
||||
queryOptions = queryOptions || {};
|
||||
queryOptions.allFacetDefinitions = ctrl.searchFacets;
|
||||
queryOptions.searchFacets = ctrl.searchFacets;
|
||||
queryOptions.defaultResourceTypes = ctrl.defaultResourceTypes;
|
||||
queryOptions.onSearchSuccess = onSearchResult;
|
||||
queryOptions.onSearchError = onSearchResult;
|
||||
|
||||
return searchlightSearchHelper.search(queryOptions);
|
||||
}
|
||||
|
||||
function onSearchResult(response) {
|
||||
ctrl.hitsSrc = response.hits;
|
||||
}
|
||||
|
||||
function actionResultHandler(result) {
|
||||
result.then(repeatUntilChangedResults);
|
||||
|
||||
function repeatUntilChangedResults() {
|
||||
// For now, all we can do is poll for a period of time.
|
||||
searchlightSearchHelper.startAdHocPolling(500, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.dashboard.project.search table controller', function () {
|
||||
|
||||
function fakeSearchlight() {
|
||||
return {
|
||||
success: function (callback) {
|
||||
callback({
|
||||
items: []
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var controller, searchlightAPI, $scope;
|
||||
|
||||
////////////////////
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
beforeEach(module('horizon.dashboard.project.search'));
|
||||
beforeEach(inject(function ($injector) {
|
||||
$scope = $injector.get('$rootScope').$new();
|
||||
|
||||
searchlightAPI = $injector.get('horizon.app.core.openstack-service-api.searchlight');
|
||||
controller = $injector.get('$controller');
|
||||
|
||||
spyOn(searchlightAPI, 'postSearch').and.callFake(fakeSearchlight);
|
||||
}));
|
||||
|
||||
function createController() {
|
||||
return controller('searchTableController', {
|
||||
searchlightAPI: searchlightAPI,
|
||||
'$scope': $scope
|
||||
});
|
||||
}
|
||||
|
||||
// I believe the following is not a valid test.
|
||||
//it('should invoke searchlightAPI apis', function () {
|
||||
// createController();
|
||||
// expect(searchlightAPI.postSearch).toHaveBeenCalled();
|
||||
//});
|
||||
|
||||
it('should set facets for search', function () {
|
||||
var ctrl = createController();
|
||||
expect(ctrl.searchFacets).toBeDefined();
|
||||
expect(ctrl.searchFacets.length).toEqual(3);
|
||||
expect(ctrl.defaultFacets[0].name).toEqual('name');
|
||||
expect(ctrl.defaultFacets[1].name).toEqual('created_at');
|
||||
expect(ctrl.defaultFacets[2].name).toEqual('updated_at');
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,134 @@
|
|||
<table ng-controller="searchTableController as table"
|
||||
hz-table ng-cloak
|
||||
st-table="table.hits"
|
||||
st-safe-src="table.hitsSrc"
|
||||
class="table table-striped table-rsp table-detail">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="100" class="search-header">
|
||||
<hz-magic-search-bar
|
||||
filter-facets="table.searchFacets"
|
||||
client-full-text-search="false"
|
||||
search-settings-callback="table.searchSettings.open">
|
||||
</hz-magic-search-bar>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<!--
|
||||
Please note, search result sorting should not be done client side.
|
||||
Searchlight will provide the proper default sorting based on search
|
||||
result scoring.
|
||||
-->
|
||||
<th class="expander"></th>
|
||||
|
||||
|
||||
<th class="rsp-p1" st-sort="type" translate>Type</th>
|
||||
<th class="rsp-p1" st-sort="name" translate>Name</th>
|
||||
<th class="rsp-p1" st-sort="status" translate>Status</th>
|
||||
<th class="rsp-p2" st-sort="updated" translate>Updated</th>
|
||||
<th class="rsp-p2" st-sort="created" translate>Created</th>
|
||||
<th class="rsp-p2" translate>Field Matches</th>
|
||||
<!-- th class="rsp-p3" translate>Description</th -->
|
||||
<th class="actions_column" translate>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr ng-repeat-start="hit in table.hits track by hit._id">
|
||||
|
||||
<td class="expander">
|
||||
<i class="fa fa-chevron-right"
|
||||
hz-expand-detail
|
||||
duration="200">
|
||||
</i>
|
||||
</td>
|
||||
|
||||
<!-- TODO Truncate not working because not in fixed container -->
|
||||
<!-- TODO ng-cloak not working -->
|
||||
<!-- TODO consider adding spinner when search is happening... -->
|
||||
<td class="rsp-p1 truncate">{$ hit._type | resourceLabeler $}</td>
|
||||
|
||||
<td ng-cloak class="rsp-p1">
|
||||
<a ng-href="{$ hit | resourceUrl $}">
|
||||
<hz-search-highlighter hit="hit"
|
||||
field="'name'"
|
||||
default-value="hit._source.name || hit._source._id | noValue">
|
||||
</hz-search-highlighter>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td class="rsp-p1">{$ hit._source.status | commonStatus | noValue $}</td>
|
||||
<td class="rsp-p2">{$ hit._source.updated_at | date:'short' | noValue $}</td>
|
||||
<td class="rsp-p2">{$ hit._source.created_at | date:'short' | noValue $}</td>
|
||||
<td class="rsp-p2">
|
||||
<ul class="list-unstyled" ng-repeat="(field, value) in hit.highlight">
|
||||
<li ng-if="!field.endsWith('.raw')">
|
||||
{$ hit._type | resourceLabeler: field $}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<!-- td class="rsp-p3 truncate">
|
||||
<hz-search-highlighter hit="hit"
|
||||
field="'description'"
|
||||
default-value="hit._source.description | noValue">
|
||||
</hz-search-highlighter>
|
||||
</td -->
|
||||
|
||||
<td class="actions_column">
|
||||
<actions allowed="table.registry.getResourceType(hit._type).itemActions"
|
||||
type="row"
|
||||
item="hit._source"
|
||||
result-handler="table.actionResultHandler">
|
||||
</actions>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat-end class="detail-row">
|
||||
<!--
|
||||
Detail-row:
|
||||
Contains detailed information on this item.
|
||||
Can be toggled using the chevron button.
|
||||
Ensure colspan is greater or equal to number of column-headers.
|
||||
-->
|
||||
<td class="detail" colspan="100">
|
||||
|
||||
<div class="row">
|
||||
<dl class="dl-horizontal">
|
||||
<div ng-repeat="(field, value) in hit._source">
|
||||
<div ng-if="['_type'].indexOf(field) === -1 && !table.isNested(value)">
|
||||
<dt data-toggle="tooltip" title="{$ field $}">
|
||||
{$ hit._type | resourceLabeler: field $}
|
||||
</dt>
|
||||
<dd>
|
||||
<hz-search-highlighter hit="hit"
|
||||
field="field"
|
||||
default-value="value | noValue">
|
||||
</hz-search-highlighter>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div ng-repeat="(field, value) in ::hit._source">
|
||||
<div ng-if="table.isNested(value)"
|
||||
class="col-md-12 detail">
|
||||
<h3> {$ hit._type | resourceLabeler: field $}</h3>
|
||||
<hr>
|
||||
<hz-array-field-table array="value">
|
||||
</hz-array-field-table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
<tfoot hz-table-footer items="table.hits"></tfoot>
|
||||
|
||||
</table>
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.directive('hzSearchHighlighter', hzSearchHighlighter);
|
||||
|
||||
hzSearchHighlighter.$inject = [
|
||||
'horizon.dashboard.project.search.basePath'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hzSearchHighlighter
|
||||
* @description
|
||||
* Takes a searchlight "hit" (search result) and if the requested "field" is highlighted
|
||||
* in the results, outputs the highlighted result. Otherwise, outputs the "default-falue".
|
||||
*
|
||||
* @param {function} basePath the base url path
|
||||
*
|
||||
* @returns {function} This directive.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <hz-search-highlighter hit="hit"
|
||||
* field="'name'"
|
||||
* default-value="hit._source.name || hit._source._id | noValue">
|
||||
* </hz-search-highlighter>
|
||||
*/
|
||||
function hzSearchHighlighter(basePath) {
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
hit: '=hit',
|
||||
field: '=field',
|
||||
defaultValue: '=defaultValue'
|
||||
},
|
||||
templateUrl: basePath + 'util/hz-search-highlighter.html'
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<div>
|
||||
<div ng-if="hit.highlight[field]"
|
||||
ng-bind-html="hit.highlight[field][0]">
|
||||
<!-- TODO - This only shows first fragment right now-->
|
||||
</div>
|
||||
<div ng-if="!hit.highlight[field]">
|
||||
{$ defaultValue $}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.resourceLocator', ResourceLocator);
|
||||
|
||||
ResourceLocator.$inject = [
|
||||
'$window'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.dashboard.project.search.resourceLocator
|
||||
* @description Locates resources in openstack dashboard.
|
||||
*
|
||||
* @param {function} $window ng $window service
|
||||
*
|
||||
* @returns {function} This service.
|
||||
*/
|
||||
function ResourceLocator($window) {
|
||||
|
||||
var service = {
|
||||
getResourceUrl: getResourceUrl
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
/**
|
||||
* @name horizon.dashboard.project.search.resourceLocator
|
||||
* @description
|
||||
* Takes a search hit and maps it to the detail page URL for it.
|
||||
*
|
||||
* @param {function} hit A search result
|
||||
*
|
||||
* @returns {function} This service.
|
||||
*/
|
||||
function getResourceUrl(hit) {
|
||||
var basePath = $window.WEBROOT;
|
||||
var idPattern = 'resourceId';
|
||||
|
||||
// TODO pull this resource type registry
|
||||
// And / or create REST API from:
|
||||
// openstack_dashboard/dashboards/project/stacks/mappings.py
|
||||
|
||||
var resourceDetailUrls = {
|
||||
'OS::Cinder::Backup': basePath + 'project/volumes/backups/' + idPattern,
|
||||
'OS::Cinder::Snapshot': basePath + 'project/volumes/snapshots/' + idPattern,
|
||||
'OS::Cinder::Volume': basePath + 'project/volumes/' + idPattern,
|
||||
'OS::Glance::Image': basePath + 'project/images/' + idPattern,
|
||||
'OS::Glance::Metadef': basePath + 'admin/metadata_defs/' + idPattern + '/detail',
|
||||
'OS::Neutron::HealthMonitor': basePath + 'project/loadbalancers/monitor/' + idPattern,
|
||||
'OS::Neutron::Net': basePath + 'project/networks/' + idPattern + '/detail',
|
||||
'OS::Neutron::Pool': basePath + 'project/loadbalancers/pool/' + idPattern,
|
||||
'OS::Neutron::PoolMember': basePath + 'project/loadbalancers/members/' + idPattern,
|
||||
'OS::Neutron::Port': basePath + 'project/networks/ports/' + idPattern + '/detail',
|
||||
'OS::Neutron::Router': basePath + 'project/routers/' + idPattern,
|
||||
'OS::Neutron::Subnet': basePath + 'project/networks/subnets/' + idPattern + '/detail',
|
||||
'OS::Nova::Server': basePath + 'project/instances/' + idPattern,
|
||||
'OS::Swift::Container': basePath + 'project/containers/' + idPattern,
|
||||
'OS::Swift::Object': basePath + 'project/containers/' + idPattern + '/' +
|
||||
idPattern + '/download',
|
||||
'OS::Designate::Zone': basePath + 'project/dns_domains/' + idPattern,
|
||||
'OS::Designate::RecordSet': basePath + 'project/dns_domains/' + idPattern +
|
||||
'/records/' + idPattern
|
||||
};
|
||||
|
||||
var result = resourceDetailUrls[hit._type] || '';
|
||||
|
||||
//TODO: Recurse up parents to find all IDs when more than one parent
|
||||
var ids = [];
|
||||
if (hit._parent) {
|
||||
ids.push(hit._parent);
|
||||
}
|
||||
// Be default we want to use the source ID, but fall back to hit._id.
|
||||
// Searchlight hit._id may include extra information appended after _.
|
||||
ids.push(hit._source.id || hit._id.split('_')[0]);
|
||||
|
||||
if (angular.isDefined(result)) {
|
||||
angular.forEach(ids, function (id) {
|
||||
result = result.replace(idPattern, id);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,400 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.searchlightFacetUtils', FacetUtils);
|
||||
|
||||
FacetUtils.$inject = [
|
||||
'commonStatusFilter',
|
||||
'resourceLabelerFilter',
|
||||
'horizon.dashboard.project.search.searchlightQueryUtils',
|
||||
'horizon.app.core.openstack-service-api.searchlight'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.dashboard.project.search.searchlightFacetUtils
|
||||
* @description Maps to / from Searchlight / Magic Search facets
|
||||
*
|
||||
* @param {function} commonStatusFilter commonStatusFilter filter
|
||||
*
|
||||
* @param {function} resourceLabeler resourceLabelerFilter
|
||||
*
|
||||
* @param {function} searchlightQueryUtils searchlightQueryUtils
|
||||
*
|
||||
* @param {function} searchlight searchlight API
|
||||
*
|
||||
* @returns {function} This service
|
||||
*/
|
||||
function FacetUtils(commonStatusFilter,
|
||||
resourceLabeler,
|
||||
searchlightQueryUtils,
|
||||
searchlight)
|
||||
{
|
||||
var service = {
|
||||
addResourceTypeFacets: addResourceTypeFacets,
|
||||
broadcastFacetsChanged: broadcastFacetsChanged,
|
||||
defaultFacets: defaultFacets,
|
||||
facetListToKeyValuePairs: facetListToKeyValuePairs,
|
||||
initScope: initScope,
|
||||
isServerFacet: isServerFacet,
|
||||
keyValueStringToKeyValueObject: keyValueStringToKeyValueObject,
|
||||
queryParamsToKeyValuePairObjects: queryParamsToKeyValuePairObjects,
|
||||
removeFacetsNotInResourceTypes: removeFacetsNotInResourceTypes,
|
||||
setTypeFacetFromResourceTypes: setTypeFacetFromResourceTypes,
|
||||
timeRangeFacetPastOptions: timeRangeFacetPastOptions,
|
||||
typeFacetFromResourceTypes: typeFacetFromResourceTypes,
|
||||
updateResourceTypeFacets: updateResourceTypeFacets
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
var scope;
|
||||
|
||||
function initScope(newScope) {
|
||||
scope = newScope;
|
||||
}
|
||||
|
||||
function defaultFacets() {
|
||||
return [
|
||||
{
|
||||
label: gettext('Name'),
|
||||
name: 'name',
|
||||
isServer: true,
|
||||
singleton: true,
|
||||
persistent: true
|
||||
},
|
||||
{
|
||||
label: gettext('Created'),
|
||||
name: 'created_at',
|
||||
singleton: true,
|
||||
isServer: true,
|
||||
persistent: true,
|
||||
options: service.timeRangeFacetPastOptions('created_at')
|
||||
},
|
||||
{
|
||||
label: gettext('Updated'),
|
||||
name: 'updated_at',
|
||||
singleton: true,
|
||||
isServer: true,
|
||||
persistent: true,
|
||||
options: service.timeRangeFacetPastOptions('updated_at')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function broadcastFacetsChanged(data) {
|
||||
scope.$broadcast('facetsChanged', data);
|
||||
}
|
||||
|
||||
function updateResourceTypeFacets(resourceTypes, allFacetDefinitions) {
|
||||
|
||||
if (angular.isString(resourceTypes)) {
|
||||
service.addResourceTypeFacets(resourceTypes, allFacetDefinitions);
|
||||
} else if (angular.isArray(resourceTypes)) {
|
||||
angular.forEach(resourceTypes, function (type) {
|
||||
service.addResourceTypeFacets(type, allFacetDefinitions);
|
||||
});
|
||||
}
|
||||
|
||||
service.removeFacetsNotInResourceTypes(resourceTypes, allFacetDefinitions);
|
||||
}
|
||||
|
||||
function addResourceTypeFacets(resourceType, allFacetDefinitions) {
|
||||
// Searchlight standardardizes on created_at / updated_at,
|
||||
// and some resource types have both.
|
||||
var skipFacets = {
|
||||
'name': {},
|
||||
'created': {},
|
||||
'created_at': {},
|
||||
'updated': {},
|
||||
'updated_at': {}
|
||||
};
|
||||
|
||||
angular.forEach(allFacetDefinitions, addSkipFacets);
|
||||
|
||||
function addSkipFacets(facet) {
|
||||
skipFacets[facet.name] = facet;
|
||||
}
|
||||
|
||||
if (!hasResourceTypeFacets(resourceType, allFacetDefinitions)) {
|
||||
searchlight.getFacets({type: resourceType}).success(updateAvailableFacetsForType);
|
||||
}
|
||||
|
||||
function updateAvailableFacetsForType(response) {
|
||||
var searchlightFacets = response[resourceType] || [];
|
||||
searchlightFacets = searchlightFacets.filter(facetNameFilter);
|
||||
|
||||
function facetNameFilter(searchlightFacet) {
|
||||
return angular.isUndefined(skipFacets[searchlightFacet.name]);
|
||||
}
|
||||
|
||||
var newFacets = convertSearchlightFacetsToMagicSearchFacets(
|
||||
resourceType, searchlightFacets);
|
||||
|
||||
if (newFacets && newFacets.length > 0) {
|
||||
Array.prototype.push.apply(allFacetDefinitions, newFacets);
|
||||
broadcastFacetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
function convertSearchlightFacetsToMagicSearchFacets(resourceType, searchlightFacets) {
|
||||
var result = searchlightFacets.map(
|
||||
function (searchlightFacet) {
|
||||
// TODO Update query sub boolean to be limited to resource type
|
||||
var facetLabel = interpolate(gettext('%(resourceType)s: %(searchFacet)s'),
|
||||
{
|
||||
resourceType: resourceLabeler(resourceType),
|
||||
searchFacet: resourceLabeler(resourceType, searchlightFacet.name)
|
||||
},
|
||||
true
|
||||
);
|
||||
var newFacet = {
|
||||
resourceType: resourceType,
|
||||
label: facetLabel,
|
||||
name: searchlightFacet.name,
|
||||
singleton: true,
|
||||
isServer: true
|
||||
};
|
||||
|
||||
if (searchlightFacet.type === 'date') {
|
||||
newFacet.options = timeRangeFacetPastOptions(searchlightFacet.name);
|
||||
} else if (searchlightFacet.options) {
|
||||
newFacet.options = convertSearchlightFacetOptionsToMagicSearchOptions(
|
||||
resourceType, searchlightFacet);
|
||||
}
|
||||
|
||||
return newFacet;
|
||||
});
|
||||
|
||||
result.sort(alphabeticalSortCompareByLabel);
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertSearchlightFacetOptionsToMagicSearchOptions(resourceType, searchlightFacet) {
|
||||
return searchlightFacet.options.map(
|
||||
function addOptions(searchlightFacetOption) {
|
||||
var option = {
|
||||
key: searchlightFacetOption.key
|
||||
};
|
||||
|
||||
if ("status" === searchlightFacet.name.toLowerCase()) {
|
||||
option.label = commonStatusFilter(searchlightFacetOption.key);
|
||||
} else {
|
||||
option.label = resourceLabeler(
|
||||
resourceType, searchlightFacetOption.key, searchlightFacet.name);
|
||||
}
|
||||
|
||||
if (searchlightFacetOption.doc_count) {
|
||||
var count = interpolate(gettext('(%(doc_count)s)'),
|
||||
{
|
||||
doc_count: searchlightFacetOption.doc_count
|
||||
},
|
||||
true
|
||||
);
|
||||
option.label = option.label + ' ' + count;
|
||||
}
|
||||
|
||||
return option;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function alphabeticalSortCompareByLabel(a, b) {
|
||||
if (a.label < b.label) {
|
||||
return -1;
|
||||
}
|
||||
if (a.label > b.label) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isServerFacet(facetName, facetDefinitions) {
|
||||
return facetDefinitions.some(isCurrentFacetServerFacet);
|
||||
|
||||
function isCurrentFacetServerFacet(facet) {
|
||||
return facet.name === facetName && facet.isServer;
|
||||
}
|
||||
}
|
||||
|
||||
function hasResourceTypeFacets(resourceType, facets) {
|
||||
return facets.some(isResourceTypeFacet);
|
||||
|
||||
function isResourceTypeFacet(facet) {
|
||||
return facet.resourceType && facet.resourceType === resourceType;
|
||||
}
|
||||
}
|
||||
|
||||
function facetListToKeyValuePairs(facets) {
|
||||
var result = [];
|
||||
if (angular.isDefined(facets)) {
|
||||
var parameterArray = facets.map(function (facet) {
|
||||
return facet.name;
|
||||
});
|
||||
parameterArray.forEach(function (param) {
|
||||
result.push(keyValueStringToKeyValueObject(param));
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function keyValueStringToKeyValueObject(keyValueString) {
|
||||
var paramSplit = keyValueString.split('=');
|
||||
var keyValuePair = {};
|
||||
if (angular.isDefined(paramSplit[1])) {
|
||||
keyValuePair[paramSplit[0]] = paramSplit[1];
|
||||
}
|
||||
return keyValuePair;
|
||||
}
|
||||
|
||||
//Note: magic search queries do NOT escape = or &, so if that is
|
||||
//in the input, it will cause problems here.
|
||||
function queryParamsToKeyValuePairObjects(urlQueryParams) {
|
||||
urlQueryParams = urlQueryParams || '';
|
||||
var result = [];
|
||||
urlQueryParams.split('&').forEach(function (param) {
|
||||
result.push(keyValueStringToKeyValueObject(param));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function removeFacetsNotInResourceTypes(resourceTypes, facets) {
|
||||
var facetsChanged = false;
|
||||
|
||||
if (resourceTypes && resourceTypes.length > 0) {
|
||||
facetsChanged = filterOutNonResourceTypesFacets(resourceTypes, facets);
|
||||
} else {
|
||||
facetsChanged = filterOutAllResourceTypeFacets(facets);
|
||||
}
|
||||
|
||||
if (facetsChanged) {
|
||||
service.broadcastFacetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
function filterOutNonResourceTypesFacets(resourceTypes, facets) {
|
||||
var facetsChanged = false;
|
||||
|
||||
// Only keep facets that are in the resource types array or in the facetsToKeep
|
||||
var resourceTypeIsArray = angular.isArray(resourceTypes);
|
||||
var currentFacet;
|
||||
for (var idx = facets.length - 1; idx >= 0; idx--) {
|
||||
currentFacet = facets[idx];
|
||||
if (!currentFacet.persistent && currentFacet.resourceType &&
|
||||
(!resourceTypeIsArray && !angular.equals(currentFacet.resourceType, resourceTypes) ||
|
||||
resourceTypeIsArray && resourceTypes.indexOf(currentFacet.resourceType) < 0)) {
|
||||
facets.splice(idx, 1);
|
||||
facetsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return facetsChanged;
|
||||
}
|
||||
|
||||
function filterOutAllResourceTypeFacets(facets) {
|
||||
var facetsChanged = false;
|
||||
|
||||
// Remove all facets specific to resource types
|
||||
for (var i = facets.length - 1; i >= 0; i--) {
|
||||
var facet = facets[i];
|
||||
if (!facet.persistent && facet.resourceType) {
|
||||
facets.splice(i, 1);
|
||||
facetsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return facetsChanged;
|
||||
}
|
||||
|
||||
function timeRangeFacetPastOptions(field) {
|
||||
return [
|
||||
{
|
||||
label: gettext('Past 10 Minutes'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-10m')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Half Hour'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-30m')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Hour'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-1h')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Day'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-1d')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Week'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-1w')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Month'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-1M')
|
||||
},
|
||||
{
|
||||
label: gettext('Past Year'),
|
||||
key: searchlightQueryUtils.getTimeRangeBoolOption('must', field, 'now-1y')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function setTypeFacetFromResourceTypes(resourceTypes, allFacetDefinitions) {
|
||||
|
||||
var updatedFacets = allFacetDefinitions.filter(typeFacetFilter);
|
||||
|
||||
function typeFacetFilter(facet) {
|
||||
return facet.name !== '_type';
|
||||
}
|
||||
|
||||
updatedFacets.unshift(typeFacetFromResourceTypes(resourceTypes));
|
||||
|
||||
allFacetDefinitions.length = 0;
|
||||
Array.prototype.push.apply(allFacetDefinitions, updatedFacets);
|
||||
}
|
||||
|
||||
function typeFacetFromResourceTypes(resourceTypes, options) {
|
||||
options = options || {};
|
||||
|
||||
var result = {
|
||||
label: gettext('Type'),
|
||||
name: '_type',
|
||||
singleton: angular.isDefined(options.singleton) ? options.singleton : false,
|
||||
isServer: true,
|
||||
options: []
|
||||
};
|
||||
|
||||
result.options = resourceTypes.map(typeToOption);
|
||||
|
||||
function typeToOption(type) {
|
||||
return {
|
||||
label: resourceLabeler(type),
|
||||
key: type
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.searchlightQueryGenerator',
|
||||
SearchlightQueryGenerator);
|
||||
|
||||
SearchlightQueryGenerator.$inject = [
|
||||
'horizon.dashboard.project.search.settingsService',
|
||||
'horizon.dashboard.project.search.searchlightQueryUtils',
|
||||
'horizon.dashboard.project.search.searchlightFacetUtils'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.dashboard.project.search.resourceLocator
|
||||
* @description Locates resources in openstack dashboard.
|
||||
*
|
||||
* @param {function} settingsService settingsService
|
||||
*
|
||||
* @param {function} searchlightQueryUtils searchlightQueryUtils
|
||||
*
|
||||
* @param {function} searchlightFacetUtils searchlightFacetUtils
|
||||
*
|
||||
* @returns {function} This service
|
||||
*/
|
||||
function SearchlightQueryGenerator(settingsService,
|
||||
searchlightQueryUtils,
|
||||
searchlightFacetUtils)
|
||||
{
|
||||
var service = {
|
||||
generate: generate
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
/*service.defaultSort = {
|
||||
_score: {order: "desc"},
|
||||
_script: {
|
||||
script: "if (doc.containsKey('updated')) { return doc['updated']; }" +
|
||||
" else if (doc.containsKey('updated_at')) { return doc['updated_at']; }" +
|
||||
" else if (doc.containsKey('created')) { return doc['created']; }" +
|
||||
" else if (doc.containsKey('created_at')) { return doc['created_at']; } " +
|
||||
" else { return 0; }",
|
||||
type: "string",
|
||||
order: "desc"
|
||||
}
|
||||
};*/
|
||||
|
||||
function generate(options) {
|
||||
options = options || {};
|
||||
|
||||
var searchlightQuery = {
|
||||
query: options.query || {}
|
||||
};
|
||||
|
||||
searchlightQuery.limit = settingsService.settings.general.limit;
|
||||
searchlightQuery.sort = options.sort;
|
||||
|
||||
var allFacetDefinitions = options.allFacetDefinitions || [];
|
||||
|
||||
parseQueryOptions(options);
|
||||
|
||||
return searchlightQuery;
|
||||
|
||||
//////////////////
|
||||
|
||||
function parseQueryOptions(options) {
|
||||
addQueryStringFromOptions(options);
|
||||
addSelectedFacetsFromOptions(options);
|
||||
addRawMagicSearchQueryFromOptions(options);
|
||||
addDefaultQuery();
|
||||
searchlightQueryUtils.addHighlighting(searchlightQuery);
|
||||
}
|
||||
|
||||
function addDefaultQuery() {
|
||||
if (angular.equals({}, searchlightQuery.query)) {
|
||||
searchlightQuery.query = {"match_all": {}};
|
||||
}
|
||||
}
|
||||
|
||||
function addSelectedFacetsFromOptions(options) {
|
||||
if (angular.isDefined(options.selectedFacets)) {
|
||||
var keyValuePairs = searchlightFacetUtils.queryParamsToKeyValuePairObjects(
|
||||
options.selectedFacets);
|
||||
addKeyValuePairsToQuery(searchlightQuery.query, keyValuePairs);
|
||||
}
|
||||
}
|
||||
|
||||
function addQueryStringFromOptions(options) {
|
||||
if (angular.isDefined(options.queryString)) {
|
||||
searchlightQueryUtils.addQueryString(searchlightQuery.query, options.queryString);
|
||||
}
|
||||
}
|
||||
|
||||
function addRawMagicSearchQueryFromOptions(options) {
|
||||
if (angular.isDefined(options.magicSearchQuery)) {
|
||||
var keyValuePairs = searchlightFacetUtils.queryParamsToKeyValuePairObjects(
|
||||
options.magicSearchQuery);
|
||||
addKeyValuePairsToQuery(keyValuePairs);
|
||||
}
|
||||
}
|
||||
|
||||
function addKeyValuePairsToQuery(keyValuePairs) {
|
||||
angular.forEach(keyValuePairs, addKeyValuePairToQuery);
|
||||
}
|
||||
|
||||
function addKeyValuePairToQuery(param) {
|
||||
var facet = {};
|
||||
facet.name = Object.keys(param)[0];
|
||||
facet.value = param[facet.name];
|
||||
|
||||
if (!searchlightFacetUtils.isServerFacet(facet.name, allFacetDefinitions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (angular.isString(facet.value) && facet.value.match('{.*}')) {
|
||||
facet.value = angular.fromJson(facet.value);
|
||||
}
|
||||
|
||||
if (searchlightQueryUtils.addSearchKeyword(searchlightQuery, facet)) {
|
||||
return;
|
||||
} else if (searchlightQueryUtils.addDefinedQueryParam(searchlightQuery.query, facet)) {
|
||||
return;
|
||||
} else {
|
||||
searchlightQueryUtils.addBestGuessBoolQueryParam(searchlightQuery.query, param, facet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}());
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.searchlightQueryUtils', SearchlightQueryUtils);
|
||||
|
||||
SearchlightQueryUtils.$inject = [
|
||||
'horizon.dashboard.project.search.settingsService'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.dashboard.project.search.resourceLocator
|
||||
* @description Locates resources in openstack dashboard.
|
||||
*
|
||||
* @param {function} settingsService settingsService
|
||||
*
|
||||
* @returns {function} This service
|
||||
*/
|
||||
function SearchlightQueryUtils(settingsService) {
|
||||
|
||||
var service = {
|
||||
addSearchKeyword: addSearchKeyword,
|
||||
addBestGuessBoolQueryParam: addBestGuessBoolQueryParam,
|
||||
addDefinedBoolParam: addDefinedBoolParam,
|
||||
addDefinedQueryParam: addDefinedQueryParam,
|
||||
addHighlighting: addHighlighting,
|
||||
addQueryString: addQueryString,
|
||||
getTimeRangeBoolOption: getTimeRangeBoolOption,
|
||||
initializeBoolQuery: initializeBoolQuery,
|
||||
search: search
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
function search(queryOptions) {
|
||||
queryOptions = queryOptions || {};
|
||||
queryOptions.allFacetDefinitions = ctrl.searchFacets;
|
||||
ctrl.lastSearchQueryOptions = queryOptions;
|
||||
|
||||
var searchlightQuery = searchlightQueryGenerator.generate(queryOptions);
|
||||
|
||||
searchlightFacetUtils.updateResourceTypeFacets(searchlightQuery.type, ctrl.searchFacets);
|
||||
|
||||
if (!searchlightQuery.type) {
|
||||
searchlightQuery.type = ctrl.defaultResourceTypes;
|
||||
}
|
||||
|
||||
return searchlight
|
||||
.postSearch(searchlightQuery, true)
|
||||
.error(onSearchSuccess);
|
||||
}
|
||||
|
||||
function initializeBoolQuery(query) {
|
||||
query = query || {};
|
||||
query.bool = query.bool || {};
|
||||
}
|
||||
|
||||
function getTimeRangeBoolOption(boolType, field, from) {
|
||||
var range = {};
|
||||
range[field] = {'from': from};
|
||||
|
||||
var option = {};
|
||||
option[boolType] = {'range': range};
|
||||
|
||||
return angular.toJson({'bool': option});
|
||||
}
|
||||
|
||||
function addSearchKeyword(searchlightQuery, facet) {
|
||||
// Return true if a search keyword is found.
|
||||
|
||||
if (angular.equals('_type', facet.name)) {
|
||||
searchlightQuery.type = searchlightQuery.type || [];
|
||||
searchlightQuery.type.push(facet.value);
|
||||
} else if (angular.equals('_sort', facet.name)) {
|
||||
searchlightQuery.sort = query.sort || [];
|
||||
searchlightQuery.sort.push(facet.value);
|
||||
} else if (angular.equals('_offset', facet.name)) {
|
||||
searchlightQuery.offset = facet.value;
|
||||
} else if (angular.equals('_source', facet.name)) {
|
||||
searchlightQuery._source = facet.value;
|
||||
} else if (angular.equals('_limit', facet.name)) {
|
||||
searchlightQuery.limit = facet.value;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function addDefinedQueryParam(query, facet) {
|
||||
if (angular.isObject(facet.value)) {
|
||||
if (facet.value.bool) {
|
||||
addDefinedBoolParam(query, facet.value.bool);
|
||||
return true;
|
||||
}
|
||||
} else if (angular.equals(facet.name, 'query_string')) {
|
||||
addQueryString(query, facet.value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function addDefinedBoolParam(query, bool) {
|
||||
initializeBoolQuery(query);
|
||||
if (bool.must) {
|
||||
query.bool.must = query.bool.must || [];
|
||||
query.bool.must.push(bool.must);
|
||||
}
|
||||
|
||||
if (bool.should) {
|
||||
query.bool.should = query.bool.should || [];
|
||||
query.bool.should.push(bool.should);
|
||||
}
|
||||
|
||||
if (bool.must_not) {
|
||||
query.bool.must_not = query.bool.must_not || [];
|
||||
query.bool.must_not.push(bool.must_not);
|
||||
}
|
||||
|
||||
if (bool.minimum_should_match) {
|
||||
query.bool.minimum_should_match = bool.minimum_should_match;
|
||||
}
|
||||
}
|
||||
|
||||
function addQueryString(query, inputQueryString) {
|
||||
//See https://www.elastic.co/guide/en/elasticsearch/
|
||||
// reference/current/query-dsl-query-string-query.html
|
||||
if (inputQueryString) {
|
||||
initializeBoolQuery(query);
|
||||
query.bool.must = query.bool.must || [];
|
||||
var queryString = {
|
||||
query_string: {
|
||||
query: inputQueryString,
|
||||
phrase_slop: settingsService.settings.fullTextSearch.phrase_slop,
|
||||
lenient: settingsService.settings.fullTextSearch.lenient,
|
||||
analyze_wildcard: settingsService.settings.fullTextSearch.analyze_wildCard
|
||||
}
|
||||
};
|
||||
query.bool.must.push(queryString);
|
||||
}
|
||||
}
|
||||
|
||||
function addBestGuessBoolQueryParam(query, param, facet) {
|
||||
initializeBoolQuery(query);
|
||||
|
||||
query.bool.must = query.bool.must || [];
|
||||
var newMust = {};
|
||||
|
||||
if (~facet.value.indexOf('~') || facet.name === 'name') {
|
||||
//TODO handle nested
|
||||
var queryString = {
|
||||
fuzzy_prefix_length: 2,
|
||||
fields: [facet.name],
|
||||
query: ~facet.value.indexOf('~') ? facet.value : facet.value + '~'
|
||||
};
|
||||
newMust.query_string = queryString;
|
||||
} else if (~facet.value.indexOf('*')) {
|
||||
newMust.wildcard = param;
|
||||
} else {
|
||||
newMust.term = param;
|
||||
}
|
||||
|
||||
if (~facet.name.indexOf('.')) {
|
||||
var nestedMust = {
|
||||
'nested': {
|
||||
'path': facet.name.split('.')[0],
|
||||
'query': newMust
|
||||
}
|
||||
};
|
||||
query.bool.must.push(nestedMust);
|
||||
} else {
|
||||
query.bool.must.push(newMust);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (TravT) inputs? Also, only do if highlighting enabled in search-settings-service.
|
||||
function addHighlighting(searchlightQuery) {
|
||||
searchlightQuery.highlight = {
|
||||
fields: {
|
||||
"*": {}
|
||||
},
|
||||
pre_tags: ["<mark>"],
|
||||
post_tags: ["</mark>"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.search')
|
||||
.factory('horizon.dashboard.project.search.searchlightSearchHelper', SearchlightSearchHelper);
|
||||
|
||||
SearchlightSearchHelper.$inject = [
|
||||
'$interval',
|
||||
'$timeout',
|
||||
'horizon.dashboard.project.search.searchlightFacetUtils',
|
||||
'horizon.dashboard.project.search.searchlightQueryGenerator',
|
||||
'horizon.app.core.openstack-service-api.searchlight',
|
||||
'horizon.dashboard.project.search.settingsService'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.dashboard.project.search.searchlightSearchHelper
|
||||
* @description Search helper - one layer above the search API for no apparent reason.
|
||||
*
|
||||
* @param {function} $interval $interval
|
||||
*
|
||||
* @param {function} $timeout $timeout
|
||||
*
|
||||
* @param {function} searchlightFacetUtils searchlightFacetUtils
|
||||
*
|
||||
* @param {function} searchlightQueryGenerator searchlightQueryGenerator
|
||||
*
|
||||
* @param {function} searchlight searchlight API
|
||||
*
|
||||
* @param {function} settingsService settings service
|
||||
*
|
||||
* @returns {function} This service
|
||||
*/
|
||||
function SearchlightSearchHelper($interval,
|
||||
$timeout,
|
||||
searchlightFacetUtils,
|
||||
searchlightQueryGenerator,
|
||||
searchlight,
|
||||
settingsService)
|
||||
{
|
||||
|
||||
var service = {
|
||||
lastSearchQueryOptions: null,
|
||||
repeatLastSearchWithLatestSettings: repeatLastSearchWithLatestSettings,
|
||||
search: search,
|
||||
startAdHocPolling: startAdHocPolling,
|
||||
stopAdHocPolling: stopAdHocPolling
|
||||
};
|
||||
|
||||
var adHocPollster = null;
|
||||
var settingsPollster = null;
|
||||
|
||||
return service;
|
||||
|
||||
//////////////////
|
||||
|
||||
function repeatLastSearchWithLatestSettings() {
|
||||
service.lastSearchQueryOptions.is_repeat = true;
|
||||
search(service.lastSearchQueryOptions);
|
||||
}
|
||||
|
||||
function search(queryOptions) {
|
||||
if (!queryOptions.is_repeat) {
|
||||
// This is a new search, stop any ad hoc polling
|
||||
// ad hoc polling is intended for attempting to
|
||||
// refresh after an action has been performed
|
||||
// and we don't have any other way to know how
|
||||
// to update the data.
|
||||
service.stopAdHocPolling();
|
||||
}
|
||||
|
||||
if (settingsPollster !== null) {
|
||||
// We just always will reset the next poll interval to
|
||||
// come after the latest search no matter what the
|
||||
// cause of the current search was.
|
||||
$timeout.cancel(settingsPollster);
|
||||
settingsPollster = null;
|
||||
}
|
||||
|
||||
service.lastSearchQueryOptions = queryOptions;
|
||||
|
||||
var searchlightQuery = searchlightQueryGenerator.generate(queryOptions);
|
||||
|
||||
if (queryOptions.searchFacets) {
|
||||
searchlightFacetUtils.updateResourceTypeFacets(
|
||||
searchlightQuery.type, queryOptions.searchFacets);
|
||||
}
|
||||
|
||||
if (!searchlightQuery.type) {
|
||||
searchlightQuery.type = queryOptions.defaultResourceTypes;
|
||||
}
|
||||
|
||||
searchlight
|
||||
.postSearch(searchlightQuery, true)
|
||||
.success(decoratedSearchSuccess)
|
||||
.error(queryOptions.onSearchError);
|
||||
|
||||
function decoratedSearchSuccess(response) {
|
||||
if (settingsService.settings.polling.enabled) {
|
||||
settingsPollster = $timeout(
|
||||
repeatLastSearchWithLatestSettings, settingsService.settings.polling.interval);
|
||||
}
|
||||
queryOptions.onSearchSuccess(response);
|
||||
}
|
||||
}
|
||||
|
||||
function startAdHocPolling(interval, maxTime) {
|
||||
stopAdHocPolling();
|
||||
interval = interval ? interval : settingsService.settings.polling.interval;
|
||||
adHocPollster = $interval(repeatLastSearchWithLatestSettings, interval);
|
||||
if (angular.isNumber(maxTime)) {
|
||||
$timeout(stopAdHocPolling, maxTime);
|
||||
}
|
||||
}
|
||||
|
||||
function stopAdHocPolling() {
|
||||
if (angular.isDefined(adHocPollster)) {
|
||||
$interval.cancel(adHocPollster);
|
||||
adHocPollster = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright 2015, Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mock
|
||||
from openstack_dashboard.api.rest import searchlight
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
# A fake requests response from searchlight's search api
|
||||
mock_es_search = mock.Mock()
|
||||
mock_es_search.json.return_value = {'total': 0, 'hits': {'hits': []}}
|
||||
|
||||
|
||||
class SearchlightRestTestCase(test.TestCase):
|
||||
@mock.patch.object(searchlight, 'searchlight_post')
|
||||
def test_run_default_search(self, sl_post):
|
||||
sl_post.return_value = mock_es_search
|
||||
request = self.mock_rest_request()
|
||||
|
||||
response = searchlight.Search().post(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual({'hits': []}, response.json)
|
||||
|
||||
expected_search = {
|
||||
'limit': 20,
|
||||
'query': {'match_all': {}}
|
||||
}
|
||||
sl_post.assert_called_with('/search', request, expected_search)
|
||||
|
||||
@mock.patch.object(searchlight, 'searchlight_post')
|
||||
def test_run_search(self, sl_post):
|
||||
sl_post.return_value = mock_es_search
|
||||
request = self.mock_rest_request(body='''{"limit": 100,
|
||||
"query": {"term": {"name": "foo"}}, "offset": 10,
|
||||
"limit": 20, "sort": {"name": "desc"}}''')
|
||||
|
||||
response = searchlight.Search().post(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual({'hits': []}, response.json)
|
||||
|
||||
expected_search = {
|
||||
'limit': 100,
|
||||
'offset': 10,
|
||||
'limit': 20,
|
||||
'query': {'term': {'name': 'foo'}},
|
||||
'sort': {'name': 'desc'}
|
||||
}
|
||||
sl_post.assert_called_with('/search', request, expected_search)
|
||||
|
||||
@mock.patch.object(searchlight, 'searchlight_get')
|
||||
def test_enabled(self, sl_get):
|
||||
mock_get_resp = mock.Mock()
|
||||
mock_get_resp.json.return_value = {
|
||||
"plugins": [
|
||||
{"name": "OS::Glance::Image",
|
||||
"index": "glance",
|
||||
"type": "OS::Glance::Image"}
|
||||
]
|
||||
}
|
||||
sl_get.return_value = mock_get_resp
|
||||
request = self.mock_rest_request()
|
||||
|
||||
response = searchlight.Plugins().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
|
||||
expected = {"plugins": [{"name": "OS::Glance::Image",
|
||||
"index": "glance",
|
||||
"type": "OS::Glance::Image"}]}
|
||||
self.assertEqual(expected, response.json)
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2010-2011 OpenStack Foundation
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslotest import base
|
||||
|
||||
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
||||
"""Test case base class for all unit tests."""
|
|
@ -0,0 +1,78 @@
|
|||
#
|
||||
# Configuration filed based on Tempest's tempest.conf.sample
|
||||
#
|
||||
|
||||
[dashboard]
|
||||
# Where the dashboard can be found (string value)
|
||||
dashboard_url=http://localhost/dashboard/
|
||||
|
||||
# Login page for the dashboard (string value)
|
||||
login_url=http://localhost/dashboard/auth/login/
|
||||
|
||||
# Dashboard help page url (string value)
|
||||
help_url=http://docs.openstack.org/
|
||||
|
||||
[selenium]
|
||||
# Timeout in seconds to wait for a page to become available
|
||||
# (integer value)
|
||||
page_timeout=30
|
||||
|
||||
# Output directory for screenshots.
|
||||
# (string value)
|
||||
screenshots_directory=integration_tests_screenshots
|
||||
|
||||
# Implicit timeout to wait until element become available,
|
||||
# this timeout is used for every find_element, find_elements call.
|
||||
# (integer value)
|
||||
implicit_wait=10
|
||||
|
||||
# Explicit timeout is used for long lasting operations,
|
||||
# methods using explicit timeout are usually prefixed with 'wait',
|
||||
# those methods ignore implicit_wait when looking up web elements.
|
||||
# (integer value)
|
||||
explicit_wait=300
|
||||
|
||||
[image]
|
||||
# http accessible image (string value)
|
||||
http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
|
||||
|
||||
[identity]
|
||||
# Username to use for non-admin API requests. (string value)
|
||||
username=demo
|
||||
|
||||
# API key to use when authenticating. (string value)
|
||||
password=secretadmin
|
||||
|
||||
# Administrative Username to use for admin API requests.
|
||||
# (string value)
|
||||
admin_username=admin
|
||||
|
||||
# API key to use when authenticating as admin. (string value)
|
||||
admin_password=secretadmin
|
||||
|
||||
[scenario]
|
||||
# ssh username for image file (string value)
|
||||
ssh_user=cirros
|
||||
|
||||
[launch_instances]
|
||||
#available zone to launch instances
|
||||
available_zone=nova
|
||||
#image_name to launch instances
|
||||
image_name=cirros-0.3.4-x86_64-uec (24.0 MB)
|
||||
|
||||
[plugin]
|
||||
|
||||
is_plugin=True
|
||||
plugin_page_path=searchlight_ui.tests.integration_tests.pages
|
||||
plugin_page_structure={
|
||||
"Project":
|
||||
{
|
||||
"Default":
|
||||
{
|
||||
"-":
|
||||
[
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import six
|
||||
|
||||
from horizon.test.settings import * # noqa
|
||||
from horizon.utils import secret_key
|
||||
from openstack_dashboard import exceptions
|
||||
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, ".."))
|
||||
|
||||
MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media'))
|
||||
MEDIA_URL = '/media/'
|
||||
STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static'))
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
SECRET_KEY = secret_key.generate_or_read_from_file(
|
||||
os.path.join(TEST_DIR, '.secret_key_store'))
|
||||
ROOT_URLCONF = 'searchlight_ui.tests.urls'
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(TEST_DIR, 'templates'),
|
||||
)
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS += (
|
||||
'openstack_dashboard.context_processors.openstack',
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.humanize',
|
||||
'django_nose',
|
||||
'openstack_auth',
|
||||
'compressor',
|
||||
'horizon',
|
||||
'openstack_dashboard',
|
||||
'openstack_dashboard.dashboards',
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
|
||||
|
||||
SITE_BRANDING = 'OpenStack'
|
||||
|
||||
HORIZON_CONFIG = {
|
||||
"password_validator": {
|
||||
"regex": '^.{8,18}$',
|
||||
"help_text": "Password must be between 8 and 18 characters."
|
||||
},
|
||||
'user_home': None,
|
||||
'help_url': "http://docs.openstack.org",
|
||||
'exceptions': {'recoverable': exceptions.RECOVERABLE,
|
||||
'not_found': exceptions.NOT_FOUND,
|
||||
'unauthorized': exceptions.UNAUTHORIZED},
|
||||
'angular_modules': [],
|
||||
'js_files': [],
|
||||
}
|
||||
|
||||
# Load the pluggable dashboard settings
|
||||
from openstack_dashboard.utils import settings
|
||||
dashboard_module_names = [
|
||||
'openstack_dashboard.enabled',
|
||||
'openstack_dashboard.local.enabled',
|
||||
'searchlight_ui.enabled',
|
||||
]
|
||||
dashboard_modules = []
|
||||
# All dashboards must be enabled for the namespace to get registered, which is
|
||||
# needed by the unit tests.
|
||||
for module_name in dashboard_module_names:
|
||||
module = importlib.import_module(module_name)
|
||||
dashboard_modules.append(module)
|
||||
for submodule in six.itervalues(settings.import_submodules(module)):
|
||||
if getattr(submodule, 'DISABLED', None):
|
||||
delattr(submodule, 'DISABLED')
|
||||
INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable
|
||||
settings.update_dashboards(dashboard_modules, HORIZON_CONFIG, INSTALLED_APPS)
|
||||
|
||||
|
||||
# Set to True to allow users to upload images to glance via Horizon server.
|
||||
# When enabled, a file form field will appear on the create image form.
|
||||
# See documentation for deployment considerations.
|
||||
HORIZON_IMAGES_ALLOW_UPLOAD = True
|
||||
|
||||
AVAILABLE_REGIONS = [
|
||||
('http://localhost:5000/v2.0', 'local'),
|
||||
('http://remote:5000/v2.0', 'remote'),
|
||||
]
|
||||
|
||||
OPENSTACK_API_VERSIONS = {
|
||||
"identity": 3
|
||||
}
|
||||
|
||||
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
|
||||
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"
|
||||
|
||||
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True
|
||||
OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'test_domain'
|
||||
|
||||
OPENSTACK_KEYSTONE_BACKEND = {
|
||||
'name': 'native',
|
||||
'can_edit_user': True,
|
||||
'can_edit_group': True,
|
||||
'can_edit_project': True,
|
||||
'can_edit_domain': True,
|
||||
'can_edit_role': True
|
||||
}
|
||||
|
||||
OPENSTACK_CINDER_FEATURES = {
|
||||
'enable_backup': True,
|
||||
}
|
||||
|
||||
OPENSTACK_NEUTRON_NETWORK = {
|
||||
'enable_lb': False,
|
||||
'enable_firewall': False,
|
||||
'enable_vpn': False
|
||||
}
|
||||
|
||||
OPENSTACK_HYPERVISOR_FEATURES = {
|
||||
'can_set_mount_point': True,
|
||||
|
||||
# NOTE: as of Grizzly this is not yet supported in Nova so enabling this
|
||||
# setting will not do anything useful
|
||||
'can_encrypt_volumes': False
|
||||
}
|
||||
|
||||
LOGGING['loggers']['openstack_dashboard'] = {
|
||||
'handlers': ['test'],
|
||||
'propagate': False,
|
||||
}
|
||||
|
||||
LOGGING['loggers']['selenium'] = {
|
||||
'handlers': ['test'],
|
||||
'propagate': False,
|
||||
}
|
||||
|
||||
LOGGING['loggers']['searchlight_ui'] = {
|
||||
'handlers': ['test'],
|
||||
'propagate': False,
|
||||
}
|
||||
|
||||
SECURITY_GROUP_RULES = {
|
||||
'all_tcp': {
|
||||
'name': 'ALL TCP',
|
||||
'ip_protocol': 'tcp',
|
||||
'from_port': '1',
|
||||
'to_port': '65535',
|
||||
},
|
||||
'http': {
|
||||
'name': 'HTTP',
|
||||
'ip_protocol': 'tcp',
|
||||
'from_port': '80',
|
||||
'to_port': '80',
|
||||
},
|
||||
}
|
||||
|
||||
NOSE_ARGS = ['--nocapture',
|
||||
'--nologcapture',
|
||||
'--cover-package=openstack_dashboard',
|
||||
'--cover-inclusive',
|
||||
'--all-modules']
|
||||
|
||||
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
|
||||
POLICY_FILES = {
|
||||
'identity': 'keystone_policy.json',
|
||||
'compute': 'nova_policy.json'
|
||||
}
|
||||
|
||||
# The openstack_auth.user.Token object isn't JSON-serializable ATM
|
||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
test_searchlight_ui
|
||||
----------------------------------
|
||||
|
||||
Tests for `searchlight_ui` module.
|
||||
"""
|
||||
|
||||
from searchlight_ui.tests import base
|
||||
|
||||
|
||||
class Test_Searchlight_ui(base.TestCase):
|
||||
|
||||
def test_something(self):
|
||||
pass
|
|
@ -0,0 +1,20 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import urls
|
||||
import openstack_dashboard.urls
|
||||
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
urls.url(r'', urls.include(openstack_dashboard.urls))
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
[metadata]
|
||||
name = searchlight-ui
|
||||
summary = Horizon panels and libraries for Searchlight
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2.7
|
||||
|
||||
[files]
|
||||
packages =
|
||||
searchlight_ui
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[compile_catalog]
|
||||
directory = searchlight_ui/locale
|
||||
domain = searchlight-ui
|
||||
|
||||
[update_catalog]
|
||||
domain = searchlight-ui
|
||||
output_dir = searchlight_ui/locale
|
||||
input_file = searchlight_ui/locale/searchlight-ui.pot
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
mapping_file = babel.cfg
|
||||
output_file = searchlight_ui/locale/searchlight-ui.pot
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=1.8'],
|
||||
pbr=True)
|
|
@ -0,0 +1,22 @@
|
|||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
hacking<0.11,>=0.10.0
|
||||
|
||||
http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon
|
||||
coverage>=3.6 # Apache-2.0
|
||||
ddt>=1.0.1 # MIT
|
||||
discover # BSD
|
||||
django-nose>=1.2 # BSD
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
nose-exclude # LGPL
|
||||
selenium>=2.50.1 # Apache-2.0
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
# This also needs xvfb library installed on your OS
|
||||
xvfbwrapper>=0.1.3 #license: MIT
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
export SEARCHLIGHT_UI_SCREENSHOTS_DIR=/opt/stack/new/searchlight-ui/.tox/py27integration/src/horizon/openstack_dashboard/test/integration_tests/integration_tests_screenshots
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script will be executed inside post_test_hook function in devstack gate
|
||||
|
||||
set -x
|
||||
|
||||
DIR=${BASH_SOURCE%/*}
|
||||
source $DIR/commons $@
|
||||
|
||||
set +e
|
||||
cd /opt/stack/new/searchlight-ui
|
||||
sudo -H -u stack tox -e py27integration
|
||||
retval=$?
|
||||
set -e
|
||||
|
||||
if [ -d ${SEARCHLIGHT_UI_SCREENSHOTS_DIR}/ ]; then
|
||||
cp -r ${SEARCHLIGHT_UI_SCREENSHOTS_DIR}/
|
||||
/home/jenkins/workspace/gate-searchlight-ui-dsvm-integration/
|
||||
fi
|
||||
exit $retval
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script will be executed inside pre_test_hook function in devstack gate
|
||||
|
||||
set -ex
|
||||
|
||||
DIR=${BASH_SOURCE%/*}
|
||||
source $DIR/commons $@
|
||||
|
||||
# Enable Searchlight UI plugin
|
||||
DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin enable_plugin searchlight http://git.openstack.org/openstack/searchlight"
|
||||
DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_service searchlight-api"
|
||||
DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_service searchlight-listener"
|
||||
DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin searchlight-ui https://git.openstack.org/openstack/searchlight-lbaas"
|
|
@ -0,0 +1,64 @@
|
|||
[tox]
|
||||
minversion = 1.6
|
||||
envlist = py27,pep8,eslint,karma
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
NOSE_WITH_OPENSTACK=1
|
||||
NOSE_OPENSTACK_COLOR=1
|
||||
NOSE_OPENSTACK_RED=0.05
|
||||
NOSE_OPENSTACK_YELLOW=0.025
|
||||
NOSE_OPENSTACK_SHOW_ELAPSED=1
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
whitelist_externals = /usr/bin/npm
|
||||
/bin/bash
|
||||
commands = python manage.py test
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:py27integration]
|
||||
basepython = python2.7
|
||||
setenv =
|
||||
INTEGRATION_TESTS=1
|
||||
SELENIUM_HEADLESS=1
|
||||
HORIZON_INTEGRATION_TESTS_CONFIG_FILE=searchlight_ui/tests/integration_tests/horizon.conf
|
||||
DJANGO_SETTINGS_MODULE=searchlight_ui.tests.settings
|
||||
commands = nosetests searchlight_ui/tests/integration_tests/tests {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py test --coverage --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
|
||||
[testenv:eslint]
|
||||
# npm must be installed on the system, for example
|
||||
# sudo apt-get install npm
|
||||
commands = npm install
|
||||
npm run lint
|
||||
|
||||
[testenv:karma]
|
||||
# npm must be installed on the system, for example
|
||||
# sudo apt-get install npm
|
||||
commands = npm install
|
||||
npm test
|
||||
|
||||
[flake8]
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
|
||||
show-source = True
|
||||
ignore = E123,E125
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
Loading…
Reference in New Issue