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:
Travis Tripp 2016-03-16 23:37:51 -06:00
parent 1b08c890c0
commit 32883ae1d2
83 changed files with 4110 additions and 27 deletions

7
.coveragerc Normal file
View File

@ -0,0 +1,7 @@
[run]
branch = True
source = searchlight_ui
omit = searchlight_ui/openstack/*
[report]
ignore_errors = True

45
.eslintrc Normal file
View File

@ -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

57
.gitignore vendored Normal file
View File

@ -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

4
.gitreview Normal file
View File

@ -0,0 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/searchlight-ui.git

3
.mailmap Normal file
View File

@ -0,0 +1,3 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>

7
.testr.conf Normal file
View File

@ -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

21
CONTRIBUTING.rst Normal file
View File

@ -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

4
HACKING.rst Normal file
View File

@ -0,0 +1,4 @@
searchlight-ui Style Commandments
===============================================
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

27
LICENSE
View File

@ -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.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

View File

@ -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

61
README.rst Normal file
View File

@ -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

2
babel.cfg Normal file
View File

@ -0,0 +1,2 @@
[python: **.py]

22
devstack/README.rst Normal file
View File

@ -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.

35
devstack/plugin.sh Normal file
View File

@ -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

3
devstack/settings Normal file
View File

@ -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

75
doc/source/conf.py Executable file
View File

@ -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}

View File

@ -0,0 +1,4 @@
============
Contributing
============
.. include:: ../../CONTRIBUTING.rst

35
doc/source/index.rst Normal file
View File

@ -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`

View File

@ -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

1
doc/source/readme.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../../README.rst

23
manage.py Normal file
View File

@ -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)

6
openstack-common.conf Normal file
View File

@ -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

28
package.json Normal file
View File

@ -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": {}
}

View File

@ -0,0 +1,3 @@
---
features:
- A unified cross resource search panel for the Mitaka release

7
requirements.txt Normal file
View File

@ -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

View File

@ -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()

View File

View File

@ -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

View File

@ -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'

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -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',)

View File

@ -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 %}

View File

@ -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'),
)

View File

@ -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'

View File

@ -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

View File

View File

@ -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
}
});
};

View File

@ -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.'));
});
}
}
}());

View File

@ -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);
});
});
});
})();

View File

@ -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;
};
}
}());

View File

@ -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');
});
});
});
})();

View File

@ -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;
};
}
})();

View File

@ -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;
};
}
})();

View File

@ -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);
};
}
}());

View File

@ -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);
}
})();

View File

@ -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/');
});
});
})();

View File

@ -0,0 +1,8 @@
table[ng-controller="searchTableController as table"] {
.detail-expanded .row {
background: none;
padding-left: 2em;
}
}

View File

@ -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
})();

View File

@ -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>

View File

@ -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);
}
}
}
})();

View File

@ -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});
});
});
})();

View File

@ -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;
}
})();

View File

@ -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>

View File

@ -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);
}
}
}
})();

View File

@ -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');
});
});
})();

View File

@ -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>

View File

@ -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;
}
})();

View File

@ -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>

View File

@ -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;
}
}
}());

View File

@ -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;
}
}
}());

View File

@ -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);
}
}
}
}
}());

View File

@ -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>"]
};
}
}
})();

View File

@ -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;
}
}
}
})();

View File

View File

@ -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)

View File

@ -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."""

View File

@ -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"
]
}
}
}

View File

@ -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'

View File

@ -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

View File

@ -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))
)

42
setup.cfg Normal file
View File

@ -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

29
setup.py Normal file
View File

@ -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)

22
test-requirements.txt Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

64
tox.ini Normal file
View File

@ -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