Commit 3c96a2ca authored by Tamika Tannis's avatar Tamika Tannis

initial commit

parents
*~
*.log
*.pyc
*.pyo
*.pyt
*.pytc
*.egg-info
.*.swp
.DS_Store
venv/
venv3/
.cache/
build/
.idea/
.coverage
.mypy_cache
.pytest_cache
npm-debug.log
.DS_Store
env
amundsen_application/.*/
dist/
node_modules/
jest
.coverage
coverage
# IntelliJ
*.iml
/.idea
# Auto-generated source maps from PyCharm
*/static/dist/webpack.*
*/static/dist/js/
.git
This project is governed by [Lyft's code of conduct](https://github.com/lyft/code-of-conduct).
All contributors and participants agree to abide by its terms.
This diff is collapsed.
recursive-include amundsen_application/static/dist *
recursive-include amundsen_application/static/fonts *
recursive-include amundsen_application/static/images *
recursive-include amundsen_application/.*/static/dist *
recursive-include amundsen_application/.*/static/fonts *
recursive-include amundsen_application/.*/static/images *
clean:
find . -name \*.pyc -delete
find . -name __pycache__ -delete
rm -rf dist/
test_unit:
python3 -bb -m pytest tests
lint:
flake8 .
test: test_unit lint
amundsenfrontendlibrary
Copyright 2018-2019 Lyft Inc.
This product includes software developed at Lyft Inc.
# Amundsen
Amundsen is a data discovery web portal. It leverages a separate [search service](https://github.com/lyft/amundsensearchlibrary) for allowing users to search for data resources, and a separate [metadata service](https://github.com/lyft/amundsenmetadatalibrary) for viewing and editing metadata for a given resource. It is a Flask application with a React frontend.
**TODO: Insert images and GIFs**
## Requirements
- Python >= 3.4
## Installation
### Install standalone application directly from the source
The following instructions are for setting up a standalone version of the Amundsen application. This approach is ideal for local development.
```bash
$ git clone https://github.com/lyft/amundsenfrontendlibrary.git
$ cd amundsenfrontendlibrary
$ python3 -m venv venv
$ source venv/bin/activate
$ pip3 install -r requirements.txt
$ python3 setup.py install
$ cd amundsen_application/static
$ npm install
$ npm run build # or npm run dev-build for un-minified source
$ cd ../..
$ python3 amundsen_application/wsgi.py
# visit http://localhost:5000 to confirm the application is running
```
You should now have the application running at http://localhost:5000, but will notice that there is no data and interactions will throw errors. The next step is to connect the standalone application to make calls to the search and metadata services.
1. Setup a local copy of the metadata service using the instructions found [here](https://github.com/lyft/amundsenmetadatalibrary).
2. Setup a local copy of the search service using the instructions found [here](https://github.com/lyft/amundsensearchlibrary).
3. Modify the `LOCAL_HOST`, `METADATA_PORT`, and `SEARCH_PORT` variables in the [LocalConfig](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/config.py) to point to where your local metadata and search services are running, and restart the application with
```bash
$ python3 amundsen_application/wsgi.py
```
### Bootstrap a default version of Amundsen using Docker
The following instructions are for setting up a version of Amundsen using Docker. At the moment, we only support a bootstrap for connecting the Amundsen application to an example metadata service.
1. Install `docker`, `docker-compose`, and `docker-machine`.
2. Start a managed docker virtual host using the following command:
```bash
# in our examples our machine is named 'default'
$ docker-machine create -d virtualbox default
```
3. Check your docker daemon locally using:
```bash
$ docker-machine ls
```
You should see the `default` machine listed, running on virtualbox with no errors listed.
4. Set up the docker environment using
```bash
$ eval $(docker-machine env default)
```
5. Setup your local environment.
* Clone [this repo](https://github.com/lyft/amundsenfrontendlibrary), [amundsenmetadatalibrary](https://github.com/lyft/amundsenmetadatalibrary), and [amundsensearchlibrary](https://github.com/lyft/amundsensearchlibrary).
* In your local versions of each library, update the `LOCAL_HOST` in the `LocalConfig` with the IP used for the `default` docker machine. You can see the IP in the `URL` outputted from running `docker-machine ls`.
* Build the docker images
```bash
# in ~/<your-path-to-cloned-repo>/amundsenmetadatalibrary
$ docker build -f public.Dockerfile -t amundsen-metadata:latest .
# in ~/<your-path-to-cloned-repo>/amundsenfrontendlibrary
$ docker build -f public.Dockerfile -t amundsen-frontend:latest .
# in ~/<your-path-to-cloned-repo>/amundsensearchlibrary
$ docker build -f public.Dockerfile -t amundsen-search:latest .
```
6. Start all of the services using:
```bash
# in ~/<your-path-to-cloned-repo>/amundsenfrontendlibrary
$ docker-compose -f docker-amundsen.yml up
```
7. Ingest dummy data into Neo4j by doing the following:
* Clone [amundsendatabuilder](https://github.com/lyft/amundsendatabuilder).
* Update the `NEO4J_ENDPOINT` in [sample_data_loader.py](https://github.com/lyft/amundsendatabuilder/blob/master/example/scripts/sample_data_loader.py) and replace `localhost` with the IP used for the `default` docker machine. You can see the IP in the `URL` outputted from running `docker-machine ls`.
* Run the following commands:
```bash
# in ~/<your-path-to-cloned-repo>/amundsendatabuilder
$ virtualenv -p python3 venv3
$ source venv3/bin/activate
$ pip3 install -r requirements.txt
$ python example/scripts/sample_data_loader.py
```
8. Verify dummy data has been ingested by viewing in Neo4j by visiting `http://YOUR-DOCKER-HOST-IP:7474/browser/` and run `MATCH (n:Table) RETURN n LIMIT 25` in the query box. You should see two tables -- `hive.core.test_driver` and `dynamo.core.test_pax`.
9. View UI at `http://YOUR-DOCKER-HOST-IP:5000/table_detail/gold/hive/core/test_driver`
## Configuration
### Flask
The default Flask application uses a [LocalConfig](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/config.py) that looks for the metadata and search services running on localhost. In order to use different end point, you need to create a custom config class suitable for your use case. Once the config class has been created, it can be referenced via the [environment variable](https://github.com/lyft/amundsenfrontendlibrary/blob/4bf244d85bf82319b14919358691fd47a094e821/amundsen_application/wsgi.py#L5): `FRONTEND_SVC_CONFIG_MODULE_CLASS`
For more information on how the configuration is being loaded and used, please reference the official Flask [documentation](http://flask.pocoo.org/docs/1.0/config/#development-production).
### React Application
#### Application Config
Certain features of the React application import variables from an [AppConfig](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/config/config.ts#L5) object. The configuration can be customized by modifying [config-custom.ts](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/config/config-custom.ts).
#### Custom Fonts & Styles
Fonts and css variables can be customized by modifying [fonts-custom.scss](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/css/_fonts-custom.scss) and
[variables-custom.scss](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/css/_variables-custom.scss).
## Developer Guide
### Environment
Follow the installation instructions in the section [Install standalone application directly from the source](https://github.com/lyft/amundsenfrontendlibrary#install-standalone-application-directly-from-the-spource).
Install the javascript development requirements:
```bash
# in ~/<your-path-to-cloned-repo>/amundsenfrontendlibrary/amundsen_application
$ cd static
$ npm install --only=dev
```
To test local changes to the javascript static files:
```bash
# in ~/<your-path-to-cloned-repo>/amundsenfrontendlibrary/amundsen_application
$ cd static
$ npm run dev-build # builds the development bundle
```
To test local changes to the python files, re-run the wsgi:
```bash
# in ~/<your-path-to-cloned-repo>/amundsenfrontendlibrary/amundsen_application
$ python3 wsgi.py
```
### Contributing
#### Python
If changes were made to any python files, run the python unit tests, linter, and type checker. Unit tests are run with `py.test`. They are located in `tests/unit`. Type checks are run with `mypy`. Linting is `flake8`. There are friendly `make` targets for each of these tests:
```bash
# after setting up environment
make test # unit tests in Python 3
make lint # flake8
make mypy # type checks
```
Fix all errors before submitting a PR.
#### JS Assets
By default, the build commands that are run to verify local changes -- `npm run build` and `npm run dev-build` -- also conduct linting and type checking. During development be sure to fix all errors before submitting a PR.
**TODO: JS unit tests are in progress - document unit test instructions after work is complete**
import ast
import importlib
import logging
import os
from flask import Flask
from amundsen_application.api import init_routes
from amundsen_application.api.v0 import blueprint
from amundsen_application.api.mail.v0 import mail_blueprint
from amundsen_application.api.metadata.v0 import metadata_blueprint
from amundsen_application.api.search.v0 import search_blueprint
app_wrapper_class = Flask
""" Support for importing a subclass of flask.Flask, via env variables """
if os.getenv('APP_WRAPPER') and os.getenv('APP_WRAPPER_CLASS'):
moduleName = os.getenv('APP_WRAPPER', '')
module = importlib.import_module(moduleName)
moduleClass = os.getenv('APP_WRAPPER_CLASS', '')
app_wrapper_class = getattr(module, moduleClass)
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = os.getenv('STATIC_ROOT', 'static')
static_dir = os.path.join(PROJECT_ROOT, STATIC_ROOT)
def create_app(config_module_class: str, template_folder: str = None) -> Flask:
""" Support for importing arguments for a subclass of flask.Flask """
args = ast.literal_eval(os.getenv('APP_WRAPPER_ARGS', '')) if os.getenv('APP_WRAPPER_ARGS') else {}
tmpl_dir = template_folder if template_folder else os.path.join(PROJECT_ROOT, static_dir, 'dist/templates')
app = app_wrapper_class(__name__, static_folder=static_dir, template_folder=tmpl_dir, **args)
""" Support for importing a custom config class """
config_module_class = \
os.getenv('FRONTEND_SVC_CONFIG_MODULE_CLASS') or config_module_class
app.config.from_object(config_module_class)
logging.basicConfig(format=app.config.get('LOG_FORMAT'), datefmt=app.config.get('LOG_DATE_FORMAT'))
logging.getLogger().setLevel(app.config.get('LOG_LEVEL'))
logging.info('Created app with config name {}'.format(config_module_class))
app.register_blueprint(blueprint)
app.register_blueprint(mail_blueprint)
app.register_blueprint(metadata_blueprint)
app.register_blueprint(search_blueprint)
init_routes(app)
return app
import os
from typing import Any, IO, Tuple
from flask import Flask, render_template, send_from_directory
from flask import current_app as app
def init_routes(app: Flask) -> None:
app.add_url_rule('/favicon.ico', 'favicon', favicon)
app.add_url_rule('/healthcheck', 'healthcheck', healthcheck)
app.add_url_rule('/', 'index', index, defaults={'path': ''}) # also functions as catch_all
app.add_url_rule('/<path:path>', 'index', index) # catch_all
def index(path: str) -> Any:
return render_template("index.html") # pragma: no cover
def healthcheck() -> Tuple[str, int]:
return '', 200 # pragma: no cover
def favicon() -> IO[bytes]:
""" TODO: Design team should provide us with a default icon """
return send_from_directory(os.path.join(app.root_path, 'static/images'),
'favicon.ico', mimetype='image/vnd.microsoft.icon') # pragma: no cover
import logging
from http import HTTPStatus
from flask import Response, jsonify, make_response, render_template, request
from flask import current_app as app
from flask.blueprints import Blueprint
from amundsen_application.log.action_log import action_logging
LOGGER = logging.getLogger(__name__)
mail_blueprint = Blueprint('mail', __name__, url_prefix='/api/mail')
@mail_blueprint.route('/feedback', methods=['POST'])
def feedback() -> Response:
""" An instance of BaseMailClient client must be configured on MAIL_CLIENT """
mail_client = app.config['MAIL_CLIENT']
if not mail_client:
message = 'An instance of BaseMailClient client must be configured on MAIL_CLIENT'
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.NOT_IMPLEMENTED)
try:
data = request.form.to_dict()
text_content = '\r\n'.join('{}:\r\n{}\r\n'.format(key, val) for key, val in data.items())
html_content = render_template('email.html', form_data=data)
# action logging
feedback_type = data.get('feedback-type')
rating = data.get('rating')
comment = data.get('comment')
bug_summary = data.get('bug-summary')
repro_steps = data.get('repro-steps')
feature_summary = data.get('feature-summary')
value_prop = data.get('value-prop')
_feedback(feedback_type=feedback_type,
rating=rating,
comment=comment,
bug_summary=bug_summary,
repro_steps=repro_steps,
feature_summary=feature_summary,
value_prop=value_prop)
response = mail_client.send_email(text=text_content, html=html_content)
status_code = response.status_code
if status_code == HTTPStatus.OK:
message = 'Success'
else:
message = 'Mail client failed with status code ' + str(status_code)
logging.error(message)
return make_response(jsonify({'msg': message}), status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)
@action_logging
def _feedback(*,
feedback_type: str,
rating: str,
comment: str,
bug_summary: str,
repro_steps: str,
feature_summary: str,
value_prop: str) -> None:
""" Logs the content of the feedback form """
pass # pragma: no cover
This diff is collapsed.
import logging
import requests
from http import HTTPStatus
from typing import Any, Dict, Optional
from flask import Response, jsonify, make_response, request
from flask import current_app as app
from flask.blueprints import Blueprint
from amundsen_application.log.action_log import action_logging
from amundsen_application.api.utils.request_utils import get_query_param
LOGGER = logging.getLogger(__name__)
REQUEST_SESSION_TIMEOUT = 10
search_blueprint = Blueprint('search', __name__, url_prefix='/api')
valid_search_fields = {
'tag',
'schema',
'table',
'column'
}
def _create_error_response(*, message: str, payload: Dict, status_code: int) -> Response:
logging.info(message)
payload['mg'] = message
return make_response(jsonify(payload), status_code)
def _validate_search_term(*, search_term: str, page_index: int) -> Optional[Response]:
# TODO: If we place these checks in the Reduc layer when actions are created/dispatched,
# we can avoid checking both here and in the search components. Ticket will be filed.
error_payload = {
'results': [],
'search_term': search_term,
'total_results': 0,
'page_index': page_index,
}
# use colon means user would like to search on specific fields
if search_term.count(':') > 1:
message = 'Encountered error: Search field should not be more than 1'
return _create_error_response(message=message, payload=error_payload, status_code=HTTPStatus.BAD_REQUEST)
if search_term.count(':') == 1:
field_key = search_term.split(' ')[0].split(':')[0]
if field_key not in valid_search_fields:
message = 'Encountered error: Search field is invalid'
return _create_error_response(message=message, payload=error_payload, status_code=HTTPStatus.BAD_REQUEST)
return None
@search_blueprint.route('/search', methods=['GET'])
def search() -> Response:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter')
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter')
error_response = _validate_search_term(search_term=search_term, page_index=int(page_index))
if error_response is not None:
return error_response
results_dict = _search(search_term=search_term, page_index=page_index)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
def _create_url_with_field(*, search_term: str, page_index: int) -> str:
"""
Construct a url by searching specific field.
E.g if we use search tag:hive test_table, search service will first
filter all the results that
don't have tag hive; then it uses test_table as query term to search /
rank all the documents.
We currently allow max 1 field.
todo: allow search multiple fields(e.g tag:hive & schema:default test_table)
:param search_term:
:param page_index:
:return:
"""
# example search_term: tag:tag_name search_term search_term2
fields = search_term.split(' ')
search_field = fields[0].split(':')
field_key = search_field[0]
# dedup tag to all lower case
field_val = search_field[1].lower()
search_term = ' '.join(fields[1:])
url = '{0}/field/{1}/field_val/{2}' \
'?page_index={3}'.format(app.config['SEARCHSERVICE_ENDPOINT'],
field_key,
field_val,
page_index)
if search_term:
url += '&query_term={0}'.format(search_term)
return url
@action_logging
def _search(*, search_term: str, page_index: int) -> Dict[str, Any]:
"""
call the search service endpoint and return matching results
:return: a json output containing search results array as 'results'
Schema Defined Here: https://github.com/lyft/
amundsensearchlibrary/blob/master/search_service/api/search.py
TODO: Define an interface for envoy_client
"""
results_dict = {
'results': [],
'search_term': search_term,
'total_results': 0,
'page_index': int(page_index),
'msg': '',
}
try:
if ':' in search_term:
url = _create_url_with_field(search_term=search_term,
page_index=page_index)
else:
url = '{0}?query_term={1}&page_index={2}'.format(app.config['SEARCHSERVICE_ENDPOINT'],
search_term,
page_index)
# TODO: Create an abstraction for this logic that is reused many times
if app.config['SEARCHSERVICE_REQUEST_CLIENT'] is not None:
envoy_client = app.config['SEARCHSERVICE_REQUEST_CLIENT']
envoy_headers = app.config['SEARCHSERVICE_REQUEST_HEADERS']
response = envoy_client.get(url, headers=envoy_headers, raw_response=True)
else:
with requests.Session() as s:
response = s.get(url, timeout=REQUEST_SESSION_TIMEOUT)
status_code = response.status_code
if status_code == HTTPStatus.OK:
results_dict['msg'] = 'Success'
results_dict['total_results'] = response.json().get('total_results')
# Filter and parse the response dictionary from the search service
params = [
'key',
'name',
'cluster',
'description',
'database',
'schema_name',
'last_updated',
]
results = response.json().get('results')
results_dict['results'] = [{key: result.get(key, None) for key in params} for result in results]
else:
message = 'Encountered error: Search request failed'
results_dict['msg'] = message
logging.error(message)
results_dict['status_code'] = status_code
return results_dict
except Exception as e:
message = 'Encountered exception: ' + str(e)
results_dict['msg'] = message
logging.exception(message)
return results_dict
from typing import Dict
def get_query_param(args: Dict, param: str, error_msg: str = None) -> str:
value = args.get(param)
if value is None:
msg = 'A {0} parameter must be provided'.format(param) if error_msg is not None else error_msg
raise Exception(msg)
return value
import json
import logging
from http import HTTPStatus
from pkg_resources import iter_entry_points
from flask import Response, jsonify, make_response, request
from flask import current_app as app
from flask.blueprints import Blueprint
# from amundsen_application.log.action_log import action_logging
from amundsen_application.models.preview_data import PreviewDataSchema
from amundsen_application.models.user import load_user
LOGGER = logging.getLogger(__name__)
# TODO: Blueprint classes might be the way to go
PREVIEW_CLIENT_CLASS = None
PREVIEW_CLIENT_INSTANCE = None
ANNOUNCEMENT_CLIENT_CLASS = None
ANNOUNCEMENT_CLIENT_INSTANCE = None
# get the preview_client_class from the python entry point
for entry_point in iter_entry_points(group='preview_client', name='table_preview_client_class'):
preview_client_class = entry_point.load()
if preview_client_class is not None:
PREVIEW_CLIENT_CLASS = preview_client_class
# get the announcement_client_class from the python entry point
for entry_point in iter_entry_points(group='announcement_client', name='announcement_client_class'):
announcement_client_class = entry_point.load()
if announcement_client_class is not None:
ANNOUNCEMENT_CLIENT_CLASS = announcement_client_class
blueprint = Blueprint('api', __name__, url_prefix='/api')
@blueprint.route('/current_user', methods=['GET'])
def current_user() -> Response:
if (app.config['CURRENT_USER_METHOD']):
user = app.config['CURRENT_USER_METHOD'](app)
else:
user = load_user({'display_name': '*'})
return user.to_json()
@blueprint.route('/preview', methods=['POST'])
def get_table_preview() -> Response:
# TODO: Want to further separate this file into more blueprints, perhaps a Blueprint class is the way to go
global PREVIEW_CLIENT_INSTANCE
try:
if PREVIEW_CLIENT_INSTANCE is None and PREVIEW_CLIENT_CLASS is not None:
PREVIEW_CLIENT_INSTANCE = PREVIEW_CLIENT_CLASS()
if PREVIEW_CLIENT_INSTANCE is None:
payload = jsonify({'previewData': {}, 'msg': 'A client for the preview feature must be configured'})
return make_response(payload, HTTPStatus.NOT_IMPLEMENTED)
# request table preview data
response = PREVIEW_CLIENT_INSTANCE.get_preview_data(params=request.get_json())
status_code = response.status_code
preview_data = json.loads(response.data).get('preview_data')
if status_code == HTTPStatus.OK:
# validate the returned table preview data
data, errors = PreviewDataSchema().load(preview_data)
if not errors:
payload = jsonify({'previewData': data, 'msg': 'Success'})
else:
logging.error('Preview data dump returned errors: ' + str(errors))
raise Exception('The preview client did not return a valid PreviewData object')
else:
message = 'Encountered error: Preview client request failed with code ' + str(status_code)
logging.error(message)
# only necessary to pass the error text
payload = jsonify({'previewData': {'error_text': preview_data.get('error_text', '')}, 'msg': message})
return make_response(payload, status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
payload = jsonify({'previewData': {}, 'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
@blueprint.route('/announcements', methods=['GET'])
def get_announcements() -> Response:
global ANNOUNCEMENT_CLIENT_INSTANCE
try:
if ANNOUNCEMENT_CLIENT_INSTANCE is None and ANNOUNCEMENT_CLIENT_CLASS is not None:
ANNOUNCEMENT_CLIENT_INSTANCE = ANNOUNCEMENT_CLIENT_CLASS()
if ANNOUNCEMENT_CLIENT_INSTANCE is None:
payload = jsonify({'posts': [], 'msg': 'A client for retrieving announcements must be configured'})
return make_response(payload, HTTPStatus.NOT_IMPLEMENTED)
return ANNOUNCEMENT_CLIENT_INSTANCE._get_posts()
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
payload = jsonify({'posts': [], 'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
import abc
import logging
from http import HTTPStatus
from flask import jsonify, make_response, Response
from amundsen_application.models.announcements import Announcements, AnnouncementsSchema
class BaseAnnouncementClient(abc.ABC):
@abc.abstractmethod
def __init__(self) -> None:
pass # pragma: no cover
@abc.abstractmethod
def get_posts(self) -> Announcements:
"""
Returns an instance of amundsen_application.models.announcements.Announcements, which should match
amundsen_application.models.announcements.AnnouncementsSchema
"""
pass # pragma: no cover
def _get_posts(self) -> Response:
def _create_error_response(message: str) -> Response:
logging.exception(message)
payload = jsonify({'posts': [], 'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
try:
try:
announcements = self.get_posts()
except Exception as e:
message = 'Encountered exception getting posts: ' + str(e)
return _create_error_response(message)
# validate the returned object
data, errors = AnnouncementsSchema().dump(announcements)
if not errors:
payload = jsonify({'posts': data.get('posts'), 'msg': 'Success'})
return make_response(payload, HTTPStatus.OK)
else:
message = 'Announcement data dump returned errors: ' + str(errors)
return _create_error_response(message)
except Exception as e:
message = 'Encountered exception: ' + str(e)
return _create_error_response(message)
import abc
from typing import List
from flask import Response
class BaseMailClient(abc.ABC):
@abc.abstractmethod
def __init__(self, recipients: List[str]) -> None:
pass # pragma: no cover
@abc.abstractmethod
def send_email(self, sender: str, recipients: List[str], subject: str, text: str, html: str) -> Response:
raise NotImplementedError # pragma: no cover
import abc
from typing import Dict
from flask import Response
class BasePreviewClient(abc.ABC):
@abc.abstractmethod
def __init__(self) -> None:
pass # pragma: no cover
@abc.abstractmethod
def get_preview_data(self, params: Dict, optionalHeaders: Dict = None) -> Response:
"""
Returns a Response object, where the response data represents a json object
with the preview data accessible on 'preview_data' key. The preview data should
match amundsen_application.models.preview_data.PreviewDataSchema
"""
raise NotImplementedError # pragma: no cover
from typing import Dict, Set # noqa: F401
class Config:
LOG_FORMAT = '%(asctime)s.%(msecs)03d [%(levelname)s] %(module)s.%(funcName)s:%(lineno)d (%(process)d:' \
+ '%(threadName)s) - %(message)s'
LOG_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
LOG_LEVEL = 'INFO'
COLUMN_STAT_ORDER = None # type: Dict[str, int]
UNEDITABLE_SCHEMAS = set() # type: Set[str]
class LocalConfig(Config):
DEBUG = False
TESTING = False
LOG_LEVEL = 'DEBUG'
# If installing locally directly from the github source
# modify these ports if necessary to point to you local search and metadata services
SEARCH_PORT = '5001'
METADATA_PORT = '5002'
# If installing using the Docker bootstrap, this should be modified to the docker host ip.
LOCAL_HOST = '0.0.0.0'
SEARCHSERVICE_REQUEST_CLIENT = None
SEARCHSERVICE_ENDPOINT = 'http://{LOCAL_HOST}:{PORT}/search'.format(LOCAL_HOST=LOCAL_HOST, PORT=SEARCH_PORT)
SEARCHSERVICE_REQUEST_HEADERS = None
METADATASERVICE_REQUEST_CLIENT = None
METADATASERVICE_POPULAR_TABLES_ENDPOINT = \
'http://{LOCAL_HOST}:{PORT}/popular_tables'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_LAST_INDEXED_ENDPOINT = \
'http://{LOCAL_HOST}:{PORT}/latest_updated_ts'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_TABLE_ENDPOINT = \
'http://{LOCAL_HOST}:{PORT}/table'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_TAGS_ENDPOINT = \
'http://{LOCAL_HOST}:{PORT}/tags'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_REQUEST_HEADERS = None
CURRENT_USER_METHOD = None
GET_PROFILE_URL = None
MAIL_CLIENT = None
import functools
import getpass
import json
import logging
import socket
from datetime import datetime, timezone, timedelta
from typing import Any, Dict, Callable
from flask import current_app as flask_app
from amundsen_application.log import action_log_callback
from amundsen_application.log.action_log_model import ActionLogParams
LOGGER = logging.getLogger(__name__)
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) # use POSIX epoch
def action_logging(f: Callable) -> Any:
"""
Decorates function to execute function at the same time triggering action logger callbacks.
It will call action logger callbacks twice, one for pre-execution and the other one for post-execution.
Action logger will be called with ActionLogParams
:param f: function instance
:return: wrapped function
"""
@functools.wraps(f)
def wrapper(*args: Any,
**kwargs: Any) -> Any:
"""
An wrapper for api functions. It creates ActionLogParams based on the function name, positional arguments,
and keyword arguments.
:param args: A passthrough positional arguments.
:param kwargs: A passthrough keyword argument
"""
metrics = _build_metrics(f.__name__, *args, **kwargs)
action_log_callback.on_pre_execution(ActionLogParams(**metrics))
output = None
try:
output = f(*args, **kwargs)
return output
except Exception as e:
metrics['error'] = e
raise
finally:
metrics['end_epoch_ms'] = get_epoch_millisec()
try:
metrics['output'] = json.dumps(output)
except Exception as e:
metrics['output'] = output
action_log_callback.on_post_execution(ActionLogParams(**metrics))
return wrapper
def get_epoch_millisec() -> int:
return (datetime.now(timezone.utc) - EPOCH) // timedelta(milliseconds=1)
def _build_metrics(func_name: str,
*args: Any,
**kwargs: Any) -> Dict[str, Any]:
"""
Builds metrics dict from function args
:param func_name:
:param args:
:param kwargs:
:return: Dict that matches ActionLogParams variable
"""
metrics = {'command': func_name} # type: Dict[str, Any]
metrics['start_epoch_ms'] = get_epoch_millisec()
metrics['host_name'] = socket.gethostname()
metrics['pos_args_json'] = json.dumps(args)
metrics['keyword_args_json'] = json.dumps(kwargs)
if flask_app.config['CURRENT_USER_METHOD']:
metrics['user'] = flask_app.config['CURRENT_USER_METHOD'](flask_app).email
else:
metrics['user'] = getpass.getuser()
return metrics
"""
An Action Logger module. Singleton pattern has been applied into this module
so that registered callbacks can be used all through the same python process.
"""
import logging
import sys
from typing import Callable, List # noqa: F401
from pkg_resources import iter_entry_points
from amundsen_application.log.action_log_model import ActionLogParams
LOGGER = logging.getLogger(__name__)
__pre_exec_callbacks = [] # type: List[Callable]
__post_exec_callbacks = [] # type: List[Callable]
def register_pre_exec_callback(action_log_callback: Callable) -> None:
"""
Registers more action_logger function callback for pre-execution. This function callback is expected to be called
with keyword args. For more about the arguments that is being passed to the callback, refer to
amundsen_application.log.action_log_model.ActionLogParams
:param action_logger: An action logger callback function
:return: None
"""
LOGGER.debug("Adding {} to pre execution callback".format(action_log_callback))
__pre_exec_callbacks.append(action_log_callback)
def register_post_exec_callback(action_log_callback: Callable) -> None:
"""
Registers more action_logger function callback for post-execution. This function callback is expected to be
called with keyword args. For more about the arguments that is being passed to the callback,
amundsen_application.log.action_log_model.ActionLogParams
:param action_logger: An action logger callback function
:return: None
"""
LOGGER.debug("Adding {} to post execution callback".format(action_log_callback))
__post_exec_callbacks.append(action_log_callback)
def on_pre_execution(action_log_params: ActionLogParams) -> None:
"""
Calls callbacks before execution.
Note that any exception from callback will be logged but won't be propagated.
:param kwargs:
:return: None
"""
LOGGER.debug("Calling callbacks: {}".format(__pre_exec_callbacks))
for call_back_function in __pre_exec_callbacks:
try:
call_back_function(action_log_params)
except Exception:
logging.exception('Failed on pre-execution callback using {}'.format(call_back_function))
def on_post_execution(action_log_params: ActionLogParams) -> None:
"""
Calls callbacks after execution. As it's being called after execution, it can capture most of fields in
amundsen_application.log.action_log_model.ActionLogParams. Note that any exception from callback will be logged
but won't be propagated.
:param kwargs:
:return: None
"""
LOGGER.debug("Calling callbacks: {}".format(__post_exec_callbacks))
for call_back_function in __post_exec_callbacks:
try:
call_back_function(action_log_params)
except Exception:
logging.exception('Failed on post-execution callback using {}'.format(call_back_function))
def logging_action_log(action_log_params: ActionLogParams) -> None:
"""
An action logger callback that just logs the ActionLogParams that it receives.
:param **kwargs keyword arguments
:return: None
"""
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug('logging_action_log: {}'.format(action_log_params))
def register_action_logs() -> None:
"""
Retrieve declared action log callbacks from entry point where there are two groups that can be registered:
1. "action_log.post_exec.plugin": callback for pre-execution
2. "action_log.pre_exec.plugin": callback for post-execution
:return: None
"""
for entry_point in iter_entry_points(group='action_log.post_exec.plugin', name=None):
print('Registering post_exec action_log entry_point: {}'.format(entry_point), file=sys.stderr)
register_post_exec_callback(entry_point.load())
for entry_point in iter_entry_points(group='action_log.pre_exec.plugin', name=None):
print('Registering pre_exec action_log entry_point: {}'.format(entry_point), file=sys.stderr)
register_pre_exec_callback(entry_point.load())
register_action_logs()
from typing import Any
class ActionLogParams(object):
"""
Holds parameters for Action log
"""
def __init__(self, *,
command: str,
start_epoch_ms: int,
end_epoch_ms: int =None,
user: str,
host_name: str,
pos_args_json: str,
keyword_args_json: str,
output: Any =None,
error: Exception =None) -> None:
self.command = command
self.start_epoch_ms = start_epoch_ms
self.end_epoch_ms = end_epoch_ms
self.user = user
self.host_name = host_name
self.pos_args_json = pos_args_json
self.keyword_args_json = keyword_args_json
self.output = output
self.error = error
def __repr__(self) -> str:
return 'ActionLogParams(command={!r}, start_epoch_ms={!r}, end_epoch_ms={!r}, user={!r}, ' \
'host_name={!r}, pos_args_json={!r}, keyword_args_json={!r}, output={!r}, error={!r})'\
.format(self.command,
self.start_epoch_ms,
self.end_epoch_ms,
self.user,
self.host_name,
self.pos_args_json,
self.keyword_args_json,
self.output,
self.error)
from marshmallow import Schema, fields, post_dump
from marshmallow.exceptions import ValidationError
from typing import Dict, List
class Post:
def __init__(self, date: str, title: str, html_content: str) -> None:
self.date = date
self.html_content = html_content
self.title = title
class PostSchema(Schema):
date = fields.Str(required=True)
title = fields.Str(required=True)
html_content = fields.Str(required=True)
class Announcements:
def __init__(self, posts: List = []) -> None:
self.posts = posts
class AnnouncementsSchema(Schema):
posts = fields.Nested(PostSchema, many=True)
@post_dump
def validate_data(self, data: Dict) -> None:
posts = data.get('posts', [])
for post in posts:
if post.get('date') is None:
raise ValidationError('All posts must have a date')
if post.get('title') is None:
raise ValidationError('All posts must have a title')
from marshmallow import Schema, fields, post_dump
from marshmallow.exceptions import ValidationError
from typing import Dict, List
class Link:
def __init__(self, label: str, href: str, target: str, use_router: bool) -> None:
self.label = label
self.href = href
self.target = target
""" Specify usage of React's built-in router vs a simple <a> anchor tag """
self.use_router = use_router
class LinksSchema(Schema):
label = fields.Str(required=True)
href = fields.Str(required=True)
target = fields.Str(required=False)
use_router = fields.Bool(required=False)
class NavLinks:
def __init__(self, links: List = []) -> None:
self.links = links
class NavLinksSchema(Schema):
links = fields.Nested(LinksSchema, many=True)
@post_dump
def validate_data(self, data: Dict) -> None:
links = data.get('links', [])
for link in links:
if link.get('label') is None:
raise ValidationError('All links must have a label')
if link.get('href') is None:
raise ValidationError('All links must have an href')
from marshmallow import Schema, fields
from typing import List
class ColumnItem:
def __init__(self, column_name: str = None, column_type: str = None) -> None:
self.column_name = column_name
self.column_type = column_type
class ColumnItemSchema(Schema):
column_name = fields.Str()
column_type = fields.Str()
class PreviewData:
def __init__(self, columns: List = [], data: List = [], error_text: str = '') -> None:
self.columns = columns
self.data = data
self.error_text = error_text
class PreviewDataSchema(Schema):
columns = fields.Nested(ColumnItemSchema, many=True)
data = fields.List(fields.Dict, many=True)
error_text = fields.Str()
from typing import Dict
from marshmallow import Schema, fields, pre_load, post_load, validates_schema, ValidationError
from flask import Response, jsonify
from flask import current_app as app
"""
TODO: Explore all internationalization use cases and
redesign how User handles names
"""
class User:
def __init__(self,
first_name: str = None,
last_name: str = None,
email: str = None,
display_name: str = None,
profile_url: str = None) -> None:
self.first_name = first_name
self.last_name = last_name
self.email = email
self.display_name = display_name
self.profile_url = profile_url
def to_json(self) -> Response:
user_info = dump_user(self)
return jsonify(user_info)
class UserSchema(Schema):
first_name = fields.Str(allow_none=True)
last_name = fields.Str(allow_none=True)
email = fields.Str(allow_none=True)
display_name = fields.Str(required=True)
profile_url = fields.Str(allow_none=True)
@pre_load
def generate_display_name(self, data: Dict) -> Dict:
if data.get('display_name', None):
return data
if data.get('first_name', None) or data.get('last_name', None):
data['display_name'] = "{} {}".format(data.get('first_name', ''), data.get('last_name', '')).strip()
return data
data['display_name'] = data.get('email', None)
return data
@pre_load
def generate_profile_url(self, data: Dict) -> Dict:
if data.get('profile_url', None):
return data
data['profile_url'] = ''
if app.config['GET_PROFILE_URL']:
data['profile_url'] = app.config['GET_PROFILE_URL'](data['display_name'])
return data
@post_load
def make_user(self, data: Dict) -> User:
return User(**data)
@validates_schema
def validate_user(self, data: Dict) -> None:
if not data.get('display_name', None):
raise ValidationError('One or more must be provided: "first_name", "last_name", "email", "display_name"')
def load_user(user_data: Dict) -> User:
try:
schema = UserSchema()
data, errors = schema.load(user_data)
return data
except ValidationError as err:
return err.messages
def dump_user(user: User) -> Dict:
schema = UserSchema()
try:
data, errors = schema.dump(user)
return data
except ValidationError as err:
return err.messages
{
"presets": ["react", "es2015", "stage-0"]
}
**/*{.,-}min.js
**/*.sh
coverage/**
dist/*
images/*
node_modules/*
node_modules*/*
stylesheets/*
vendor/*
docs/*
appbuilder/*
{
"extends": "airbnb",
"parserOptions":{
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"globals": {
"document": true,
},
"rules": {
"prefer-template": 0,
"new-cap": 0,
"no-restricted-syntax": 0,
"guard-for-in": 0,
"prefer-arrow-callback": 0,
"func-names": 0,
"react/jsx-no-bind": 0,
"no-confusing-arrow": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/anchor-has-content": 0,
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "to", "hrefLeft", "hrefRight" ],
"aspects": [ "noHref", "invalidHref", "preferButton" ]
}],
"react/require-default-props": 0,
"no-plusplus": 0,
"no-mixed-operators": 0,
"no-continue": 0,
"no-bitwise": 0,
"no-undef": 0,
"no-multi-assign": 0,
"react/no-array-index-key": 0,
"no-restricted-properties": 0,
"no-prototype-builtins": 0,
"jsx-a11y/href-no-hash": 0,
"react/forbid-prop-types": 0,
"class-methods-use-this": 0,
"import/extensions": 0,
"import/no-named-as-default": 0,
"import/no-extraneous-dependencies": 0,
"import/no-unresolved": 0,
"import/prefer-default-export": 0,
"react/no-unescaped-entities": 0,
"react/no-unused-prop-types": 0,
"react/no-string-refs": 0,
"indent": 0,
"no-multi-spaces": 0,
"padded-blocks": 0,
}
}
// This file should be used to add new config variables or overwrite defaults from config-default.ts
import { AppConfigCustom } from './config.types';
const configCustom: AppConfigCustom = {
browse: {
curatedTags: [],
showAllTags: true,
},
google: {
key: 'default-key',
sampleRate: 100,
},
};
export default configCustom;
import { AppConfig } from "./config.types";
const configDefault: AppConfig = {
browse: {
curatedTags: [],
showAllTags: true,
},
exploreSql: {
enabled: false,
generateUrl: (database: string, cluster: string, schema: string, table: string, partitionKey?: string, partitionValue?: string) => {
return `https://DEFAULT_EXPLORE_URL?schema=${schema}&cluster=${cluster}&db=${database}&table=${table}`;
}
},
google: {
key: 'default-key',
sampleRate: 100,
},
navLinks: [
{
label: "Announcements",
href: "/announcements",
use_router: true,
},
{
label: "Browse",
href: "/browse",
use_router: true,
}
]
};
export default configDefault;
import { AppConfig } from './config.types';
import configDefault from './config-default';
import configCustom from './config-custom';
// This is not a shallow merge. Any defined members of customConfig will override configDefault.
const appConfig: AppConfig = { ...configDefault, ...configCustom };
export default appConfig;
/**
* AppConfig and AppConfigCustom should share the same definition, except each field in AppConfigCustom
* is optional. If you choose to override one of the configs, you must provide the full type definition
* for that section.
*/
export interface AppConfig {
browse: BrowseConfig;
exploreSql: ExploreSqlConfig;
google: GoogleAnalyticsConfig;
navLinks: Array<LinkConfig>;
}
export interface AppConfigCustom {
google?: GoogleAnalyticsConfig
browse?: BrowseConfig;
exploreSql?: ExploreSqlConfig;
navLinks?: Array<LinkConfig>;
}
interface GoogleAnalyticsConfig {
key: string;
sampleRate: number;
}
interface BrowseConfig {
curatedTags: Array<string>;
showAllTags: boolean;
}
interface ExploreSqlConfig {
enabled: boolean;
generateUrl: (database: string, cluster: string, schema: string, table: string, partitionKey?: string, partitionValue?: string) => string;
}
interface LinkConfig {
href: string;
label: string;
target?: string;
use_router: boolean;
}
$icon-font-path: '/static/fonts/bootstrap/';
// Bootstrap + Custom variables
@import 'variables';
// Core mixins
@import '~bootstrap-sass/assets/stylesheets/bootstrap/mixins';
// Reset and dependencies
@import '~bootstrap-sass/assets/stylesheets/bootstrap/normalize';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/print';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/glyphicons';
// Core CSS
@import '~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/type';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/code';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/grid';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/tables';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/forms';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/buttons';
// Components
@import '~bootstrap-sass/assets/stylesheets/bootstrap/component-animations';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/dropdowns';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/button-groups';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/input-groups';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/navs';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/navbar';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/breadcrumbs';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/pagination';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/pager';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/labels';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/badges';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/jumbotron';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/thumbnails';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/alerts';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/progress-bars';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/media';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/list-group';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/panels';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/responsive-embed';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/wells';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/close';
// Components w/ JavaScript
@import '~bootstrap-sass/assets/stylesheets/bootstrap/modals';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/tooltip';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/popovers';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/carousel';
// Utility classes
@import '~bootstrap-sass/assets/stylesheets/bootstrap/utilities';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities';
// This file is intentionally left blank and should be overwritten by in the build process.
@import 'variables';
.btn {
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
background-color: $gradient-1;
border-color: $gradient-2;
outline: none;
&.icon {
background-color: $gradient-4;
}
}
* {
vertical-align: middle;
}
img {
border: none;
height: 24px;
margin: -3px 4px -3px 0px;
min-width: 24px;
width: 24px;
}
&.btn-block {
margin-bottom: 4px;
}
&.btn-primary {
border-width: 2px;
-webkit-box-shadow: 8px 9px 20px -15px rgba(0,0,0,0.46);
-moz-box-shadow: 8px 9px 20px -15px rgba(0,0,0,0.46);
box-shadow: 8px 9px 20px -15px rgba(0,0,0,0.46);
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
}
&.disabled,
&:disabled {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
color: $gray-light;
pointer-events: none;
&:hover {
color: $gray-light;
}
}
}
@import 'buttons-default';
@import 'buttons-custom';
// This file is intentionally left blank and should be used to add new fonts or overwrite defaults from _fonts-default.scss
// TODO - Import Open Sans or other default font
// Amundsen Default Fonts
@import 'fonts-default';
// Per-Client Custom Fonts
@import 'fonts-custom';
@import 'variables';
img.icon {
background-color: $gray-lighter;
border: none;
height: 24px;
margin: -3px 4px -3px 0px;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
min-width: 24px;
width: 24px;
&.icon-color {
background-color: $gradient-3;
}
// TODO - Add other icons here
&.icon-database {
-webkit-mask-image: url('/static/images/icons/Database.svg');
mask-image: url('/static/images/icons/Database.svg');
}
&.icon-loading {
-webkit-mask-image: url('/static/images/icons/Loader.svg');
mask-image: url('/static/images/icons/Loader.svg');
}
&.icon-preview {
-webkit-mask-image: url('/static/images/icons/Preview.svg');
mask-image: url('/static/images/icons/Preview.svg');
}
&.icon-right {
-webkit-mask-image: url('/static/images/icons/Right.svg');
mask-image: url('/static/images/icons/Right.svg');
}
}
.btn img.icon {
// Some icons need to be scaled down when inside buttons
&.icon-database {
-webkit-mask-position: 3px 3px;
mask-position: 3px 3px;
-webkit-mask-size: 18px 18px;
mask-size: 18px 18px;
}
}
.disabled,
:disabled {
> img.icon,
> img.icon.icon-color {
background-color: $gray-light;
}
}
// TODO - Try to implement this with bootstrap variables
@import 'variables';
.list-group-item {
cursor: pointer;
border-top-color: $gray-lighter;
border-bottom-color: $gray-lighter;
border-left: none;
border-right: none;
padding-left: 4px;
padding-right: 4px;
}
.list-group-item:hover {
border-top-color: $gradient-2;
border-bottom-color: $gradient-2;
background-color: $gradient-1;
}
.list-group-item:hover + li {
border-top-color: $gradient-2;
}
.list-group-item:hover + li:not(:last-child) {
border-bottom-color: $gradient-2;
}
// TODO - Override Bootstrap variables and delete this.
@import 'variables';
.popover {
background-color: $gray-darker;
border: 1px solid $gray-darker;
color: $gray-lighter;
font-size: 12px;
padding: 5px;
}
.popover-title {
background-color: $gray-darker;
border-bottom: 1px solid $gray-light;
color: $gray-lighter;
font-size: 14px;
padding: 5px;
}
.popover-content {
padding: 2px 5px;
}
.popover.right .arrow:after {
border-right-color: $gray-darker;
}
.popover.bottom .arrow:after {
border-bottom-color: $gray-darker;
}
.popover.top .arrow:after {
border-top-color: $gray-darker;
}
.popover.left .arrow:after {
border-left-color: $gray-darker;
}
// This file is intentionally left blank and should be used to add new custom variables or overwrite defaults from _variables-default.scss
/**
The following are used in Amundsen but are not part of Bootstrap.
These must be defined here:
$gradient-1, $gradient-2, $gradient-3, $gradient-4,
$font-family-sans-serif-bold,
$font-weight-sans-serif-bold
*/
// Colors
$gradient-1: #f2f5fe;
$gradient-2: #cad6ff;
$gradient-3: #5679ff;
$gradient-4: #3250c8;
$gray-base: #000;
$gray-darker: lighten($gray-base, 10%); // #1a1a1a
$gray-dark: lighten($gray-base, 20%); // #333
$gray: lighten($gray-base, 35%); // ##595959
$gray-light: lighten($gray-base, 55%); // #8c8c8c
$gray-lighter: lighten($gray-base, 85%); // #d9d9d9
// Scaffolding
$body-bg: #fff;
$text-color: $gray-dark;
// Typography
$font-family-sans-serif: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
$font-family-sans-serif-bold: $font-family-sans-serif;
$font-weight-sans-serif-bold: 600;
$font-family-serif: Georgia, "Times New Roman", Times, serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-family-base: $font-family-sans-serif;
$headings-font-family: $font-family-sans-serif-bold;
$font-size-small: 12px;
$line-height-small: 18px;
$font-size-large: 16px;
$line-height-large: 24px;
// Buttons
$btn-default-border: $gray-lighter;
$btn-default-bg: white;
$btn-default-color: $text-color;
$btn-primary-border: $gray-lighter;
$btn-primary-bg: white;
$btn-primary-color: $text-color;
// Bootstrap Default Values
@import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
// Amundsen Default Values
@import 'variables-default';
// Per-Client Custom Values
@import 'variables-custom';
@import 'bootstrap-custom';
@import 'buttons';
@import 'fonts';
@import 'icons';
@import 'list-group-item';
@import 'popovers';
// TODO - Find a better place for misc styles.
td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
form {
margin-bottom: 0;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
declare var require: {
<T>(path: string): T;
(paths: string[], callback: (...modules: any[]) => void): void;
ensure: (
paths: string[],
callback: (require: <T>(path: string) => T) => void
) => void;
};
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Close</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M13.4142136,12 L18.7071068,17.2928932 C19.0976311,17.6834175 19.0976311,18.3165825 18.7071068,18.7071068 C18.3165825,19.0976311 17.6834175,19.0976311 17.2928932,18.7071068 L12,13.4142136 L6.70710678,18.7071068 C6.31658249,19.0976311 5.68341751,19.0976311 5.29289322,18.7071068 C4.90236893,18.3165825 4.90236893,17.6834175 5.29289322,17.2928932 L10.5857864,12 L5.29289322,6.70710678 C4.90236893,6.31658249 4.90236893,5.68341751 5.29289322,5.29289322 C5.68341751,4.90236893 6.31658249,4.90236893 6.70710678,5.29289322 L12,10.5857864 L17.2928932,5.29289322 C17.6834175,4.90236893 18.3165825,4.90236893 18.7071068,5.29289322 C19.0976311,5.68341751 19.0976311,6.31658249 18.7071068,6.70710678 L13.4142136,12 Z" id="path-1"></path>
</defs>
<g id="Close" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="x" mask="url(#mask-2)">
<g transform="translate(6.000000, 6.000000)"></g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Database</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M20,7.52769756 C18.1751048,8.47599017 15.2701244,9 12,9 C8.7298756,9 5.82489521,8.47599017 4,7.52769756 L4,12.0000001 C4,12.8127884 7.5561637,14 12,14 C16.4438363,14 20,12.8127884 20,12 L20,7.52769756 Z M22,5 L22,19 C22,21.511827 17.5423388,23 12,23 C6.45766119,23 2,21.511827 2,19 L2,5 C2,2.49324457 6.4797337,1 12,1 C17.5202663,1 22,2.49324457 22,5 Z M20,14.5332627 C18.1788421,15.4790145 15.2792852,16 12,16 C8.72071483,16 5.82115791,15.4790145 4,14.5332627 L4,19 C4,19.8127884 7.5561637,21 12,21 C16.4438363,21 20,19.8127884 20,19 L20,14.5332627 Z M12,7 C16.4208592,7 20,5.80695307 20,5 C20,4.19304693 16.4208592,3 12,3 C7.5791408,3 4,4.19304693 4,5 C4,5.80695307 7.5791408,7 12,7 Z" id="path-1"></path>
</defs>
<g id="Database" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Delta-Down</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12.6,6.8 L20.1,16.8 C20.3485281,17.1313708 20.2813708,17.6014719 19.95,17.85 C19.8201779,17.9473666 19.6622777,18 19.5,18 L4.5,18 C4.08578644,18 3.75,17.6642136 3.75,17.25 C3.75,17.0877223 3.8026334,16.9298221 3.9,16.8 L11.4,6.8 C11.6485281,6.46862915 12.1186292,6.40147186 12.45,6.65 C12.5068542,6.69264069 12.5573593,6.74314575 12.6,6.8 Z" id="path-1"></path>
</defs>
<g id="Delta-Down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Triangle-2" fill="#11111F" transform="translate(12.000000, 12.000000) scale(-1, -1) translate(-12.000000, -12.000000) " xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="triangle" mask="url(#mask-2)">
<g transform="translate(1.000000, 2.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Delta-Up</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12.6,6.8 L20.1,16.8 C20.3485281,17.1313708 20.2813708,17.6014719 19.95,17.85 C19.8201779,17.9473666 19.6622777,18 19.5,18 L4.5,18 C4.08578644,18 3.75,17.6642136 3.75,17.25 C3.75,17.0877223 3.8026334,16.9298221 3.9,16.8 L11.4,6.8 C11.6485281,6.46862915 12.1186292,6.40147186 12.45,6.65 C12.5068542,6.69264069 12.5573593,6.74314575 12.6,6.8 Z" id="path-1"></path>
</defs>
<g id="Delta-Up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Triangle-2" fill="#11111F" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="triangle" mask="url(#mask-2)">
<g transform="translate(3.000000, 6.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Dimension</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.5527864,4.10557281 C11.8343139,3.96480906 12.1656861,3.96480906 12.4472136,4.10557281 L19.4472136,7.60557281 C20.1842621,7.97409708 20.1842621,9.02590292 19.4472136,9.39442719 L12.4472136,12.8944272 C12.1656861,13.0351909 11.8343139,13.0351909 11.5527864,12.8944272 L4.5527864,9.39442719 C3.81573787,9.02590292 3.81573787,7.97409708 4.5527864,7.60557281 L11.5527864,4.10557281 Z M12,6.11803399 L7.23606798,8.5 L12,10.881966 L16.763932,8.5 L12,6.11803399 Z M18.5527864,14.6055728 C19.0467649,14.3585836 19.6474379,14.5588079 19.8944272,15.0527864 C20.1414164,15.5467649 19.9411921,16.1474379 19.4472136,16.3944272 L12.4472136,19.8944272 C12.1656861,20.0351909 11.8343139,20.0351909 11.5527864,19.8944272 L4.5527864,16.3944272 C4.05880791,16.1474379 3.85858356,15.5467649 4.10557281,15.0527864 C4.35256206,14.5588079 4.9532351,14.3585836 5.4472136,14.6055728 L12,17.881966 L18.5527864,14.6055728 Z M18.5527864,11.1055728 C19.0467649,10.8585836 19.6474379,11.0588079 19.8944272,11.5527864 C20.1414164,12.0467649 19.9411921,12.6474379 19.4472136,12.8944272 L12.4472136,16.3944272 C12.1656861,16.5351909 11.8343139,16.5351909 11.5527864,16.3944272 L4.5527864,12.8944272 C4.05880791,12.6474379 3.85858356,12.0467649 4.10557281,11.5527864 C4.35256206,11.0588079 4.9532351,10.8585836 5.4472136,11.1055728 L12,14.381966 L18.5527864,11.1055728 Z" id="path-1"></path>
</defs>
<g id="Dimension" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Down Arrow</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11,7.92054674 L7.54660817,10.6832602 C7.16925397,10.9851436 6.61862315,10.9239624 6.31673979,10.5466082 C6.01485644,10.169254 6.07603764,9.61862315 6.45339183,9.31673979 L11.4533918,5.31673979 C11.7729582,5.06108674 12.2270418,5.06108674 12.5466082,5.31673979 L17.5466082,9.31673979 C17.9239624,9.61862315 17.9851436,10.169254 17.6832602,10.5466082 C17.3813769,10.9239624 16.830746,10.9851436 16.4533918,10.6832602 L13,7.92054674 L13,17 C13,17.6642136 12.5522847,18 12,18 C11.4477153,18 11,17.6642136 11,17 L11,7.92054674 Z" id="path-1"></path>
</defs>
<g id="Down-Arrow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" transform="translate(12.000000, 11.562500) scale(1, -1) translate(-12.000000, -11.562500) " xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Down-Arrow</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11,7.92054674 L7.54660817,10.6832602 C7.16925397,10.9851436 6.61862315,10.9239624 6.31673979,10.5466082 C6.01485644,10.169254 6.07603764,9.61862315 6.45339183,9.31673979 L11.4533918,5.31673979 C11.7729582,5.06108674 12.2270418,5.06108674 12.5466082,5.31673979 L17.5466082,9.31673979 C17.9239624,9.61862315 17.9851436,10.169254 17.6832602,10.5466082 C17.3813769,10.9239624 16.830746,10.9851436 16.4533918,10.6832602 L13,7.92054674 L13,17 C13,17.6642136 12.5522847,18 12,18 C11.4477153,18 11,17.6642136 11,17 L11,7.92054674 Z" id="path-1"></path>
</defs>
<g id="Down-Arrow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" transform="translate(12.000000, 11.562500) scale(1, -1) translate(-12.000000, -11.562500) " xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Down</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.9873073,13.7302417 L16.0380559,9.81237587 C16.4369613,9.42655632 17.071544,9.43184703 17.463961,9.82426407 C17.8480273,10.2083304 17.8480273,10.8310249 17.463961,11.2150912 C17.4600959,11.2189563 17.4561985,11.2227892 17.4522694,11.2265894 L12.7187984,15.8047814 C12.5412869,15.9764697 12.3171062,16.0707097 12.0883633,16.0878523 C11.7999983,16.1178672 11.5010724,16.0260245 11.2749965,15.8112549 L6.44216084,11.220116 C6.05189004,10.8493632 6.0360676,10.2324316 6.40682041,9.84216084 C6.41255824,9.83612094 6.41837327,9.83015487 6.42426407,9.82426407 C6.8176871,9.43084104 7.45299389,9.42269552 7.8563744,9.80590242 L11.9873073,13.7302417 Z" id="path-1"></path>
</defs>
<g id="Down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="mask" fill="#D6D9DB" xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Edit</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M17,14 C17,13.4477153 17.4477153,13 18,13 C18.5522847,13 19,13.4477153 19,14 L19,17 C19,18.5522847 17.5522847,20 16,20 L7,20 C5.41405389,20 4,18.644016 4,17 L4,7.98046875 C4,6.44299261 5.45244866,5 7,5 L10,5 C10.5522847,5 11,5.44771525 11,6 C11,6.55228475 10.5522847,7 10,7 L7,7 C6.55410442,7 6,7.55049697 6,7.98046875 L6,17 C6,17.5202489 6.50029637,18 7,18 L16,18 C16.4477153,18 17,17.4477153 17,17 L17,14 Z M17.9289109,5.91662172 C18.3682507,6.35596155 18.3682507,7.06827215 17.9289109,7.50761198 L10.1018764,15.3159836 L8.45489769,15.8649765 C8.25841894,15.9304695 8.04604895,15.8242845 7.98055604,15.6278057 C7.95489769,15.5508307 7.95489769,15.4676099 7.98055604,15.3906349 L8.559746,13.6947963 L16.3379206,5.91662172 C16.7772604,5.47728189 17.489571,5.47728189 17.9289109,5.91662172 Z" id="path-1"></path>
</defs>
<g id="Edit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)">
<g transform="translate(2.000000, 2.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Edit</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M17,14 C17,13.4477153 17.4477153,13 18,13 C18.5522847,13 19,13.4477153 19,14 L19,17 C19,18.5522847 17.5522847,20 16,20 L7,20 C5.41405389,20 4,18.644016 4,17 L4,7.98046875 C4,6.44299261 5.45244866,5 7,5 L10,5 C10.5522847,5 11,5.44771525 11,6 C11,6.55228475 10.5522847,7 10,7 L7,7 C6.55410442,7 6,7.55049697 6,7.98046875 L6,17 C6,17.5202489 6.50029637,18 7,18 L16,18 C16.4477153,18 17,17.4477153 17,17 L17,14 Z M17.9289109,5.91662172 C18.3682507,6.35596155 18.3682507,7.06827215 17.9289109,7.50761198 L10.1018764,15.3159836 L8.45489769,15.8649765 C8.25841894,15.9304695 8.04604895,15.8242845 7.98055604,15.6278057 C7.95489769,15.5508307 7.95489769,15.4676099 7.98055604,15.3906349 L8.559746,13.6947963 L16.3379206,5.91662172 C16.7772604,5.47728189 17.489571,5.47728189 17.9289109,5.91662172 Z" id="path-1"></path>
</defs>
<g id="Edit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#785EF0" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)">
<g transform="translate(2.000000, 2.000000)"></g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Expand</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11,11 L11,8 C11,7.44771525 11.4477153,7 12,7 C12.5522847,7 13,7.44771525 13,8 L13,11 L16,11 C16.5522847,11 17,11.4477153 17,12 C17,12.5522847 16.5522847,13 16,13 L13,13 L13,16 C13,16.5522847 12.5522847,17 12,17 C11.4477153,17 11,16.5522847 11,16 L11,13 L8,13 C7.44771525,13 7,12.5522847 7,12 C7,11.4477153 7.44771525,11 8,11 L11,11 Z M5,2 L19,2 C20.6568542,2 22,3.34314575 22,5 L22,19 C22,20.6568542 20.6568542,22 19,22 L5,22 C3.34314575,22 2,20.6568542 2,19 L2,5 C2,3.34314575 3.34314575,2 5,2 Z M5,4 C4.44771525,4 4,4.44771525 4,5 L4,19 C4,19.5522847 4.44771525,20 5,20 L19,20 C19.5522847,20 20,19.5522847 20,19 L20,5 C20,4.44771525 19.5522847,4 19,4 L5,4 Z" id="path-1"></path>
</defs>
<g id="Expand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Favorite-Filled</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12,18 L7.4472136,20.2763932 C6.9532351,20.5233825 6.35256206,20.3231581 6.10557281,19.8291796 C6.03614509,19.6903242 6,19.5372111 6,19.381966 L6,14 L2.93871978,10.1733997 C2.59371023,9.74213779 2.6636316,9.11284541 3.09489354,8.76783586 C3.19842229,8.68501287 3.31738718,8.62360366 3.44486746,8.58718073 L9,7 L11.1055728,2.78885438 C11.3525621,2.29487588 11.9532351,2.09465154 12.4472136,2.34164079 C12.640741,2.43840449 12.7976635,2.59532698 12.8944272,2.78885438 L15,7 L20.5551325,8.58718073 C21.0861676,8.73890502 21.3936597,9.29239079 21.2419354,9.8234258 C21.2055124,9.95090608 21.1441032,10.069871 21.0612802,10.1733997 L18,14 L18,19.381966 C18,19.9342508 17.5522847,20.381966 17,20.381966 C16.8447549,20.381966 16.6916418,20.3458209 16.5527864,20.2763932 L12,18 Z" id="path-1"></path>
</defs>
<g id="Favorite-Filled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Favorite" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Favorite</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M16,13.2984379 L18.5608228,10.0974093 L13.6046057,8.6813473 L12,5.47213595 L10.3953943,8.6813473 L5.43917717,10.0974093 L8,13.2984379 L8,17.763932 L12,15.763932 L16,17.763932 L16,13.2984379 Z M12,18 L7.4472136,20.2763932 C6.9532351,20.5233825 6.35256206,20.3231581 6.10557281,19.8291796 C6.03614509,19.6903242 6,19.5372111 6,19.381966 L6,14 L2.93871978,10.1733997 C2.59371023,9.74213779 2.6636316,9.11284541 3.09489354,8.76783586 C3.19842229,8.68501287 3.31738718,8.62360366 3.44486746,8.58718073 L9,7 L11.1055728,2.78885438 C11.3525621,2.29487588 11.9532351,2.09465154 12.4472136,2.34164079 C12.640741,2.43840449 12.7976635,2.59532698 12.8944272,2.78885438 L15,7 L20.5551325,8.58718073 C21.0861676,8.73890502 21.3936597,9.29239079 21.2419354,9.8234258 C21.2055124,9.95090608 21.1441032,10.069871 21.0612802,10.1733997 L18,14 L18,19.381966 C18,19.9342508 17.5522847,20.381966 17,20.381966 C16.8447549,20.381966 16.6916418,20.3458209 16.5527864,20.2763932 L12,18 Z" id="path-1"></path>
</defs>
<g id="Favorite" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Info-Filled</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12,21 C7.02943725,21 3,16.9705627 3,12 C3,7.02943725 7.02943725,3 12,3 C16.9705627,3 21,7.02943725 21,12 C21,16.9705627 16.9705627,21 12,21 Z M13,15.1666667 L13,11.8333333 C13,11.373096 12.5522847,11 12,11 C11.4477153,11 11,11.373096 11,11.8333333 L11,15.1666667 C11,15.626904 11.4477153,16 12,16 C12.5522847,16 13,15.626904 13,15.1666667 Z M12,10 C12.5522847,10 13,9.55228475 13,9 C13,8.44771525 12.5522847,8 12,8 C11.4477153,8 11,8.44771525 11,9 C11,9.55228475 11.4477153,10 12,10 Z" id="path-1"></path>
</defs>
<g id="Info-Filled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#D6D9DB" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Info</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12,21 C7.02943725,21 3,16.9705627 3,12 C3,7.02943725 7.02943725,3 12,3 C16.9705627,3 21,7.02943725 21,12 C21,16.9705627 16.9705627,21 12,21 Z M12,19 C15.8659932,19 19,15.8659932 19,12 C19,8.13400675 15.8659932,5 12,5 C8.13400675,5 5,8.13400675 5,12 C5,15.8659932 8.13400675,19 12,19 Z M13,15.1666667 C13,15.626904 12.5522847,16 12,16 C11.4477153,16 11,15.626904 11,15.1666667 L11,11.8333333 C11,11.373096 11.4477153,11 12,11 C12.5522847,11 13,11.373096 13,11.8333333 L13,15.1666667 Z M12,10 C11.4477153,10 11,9.55228475 11,9 C11,8.44771525 11.4477153,8 12,8 C12.5522847,8 13,8.44771525 13,9 C13,9.55228475 12.5522847,10 12,10 Z" id="path-1"></path>
</defs>
<g id="Info" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#D6D9DB" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Left</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.6936353,13.00033 L15.8296357,9.01309792 C16.2293602,8.62775123 16.8640771,8.63356283 17.2566785,9.02616421 C17.6401162,9.40960195 17.6401162,10.0312774 17.2566785,10.4147151 C17.2524406,10.418953 17.2481641,10.4231519 17.2438493,10.4273115 L12.4008785,15.0960848 C12.1644542,15.3240051 11.8458212,15.4150847 11.5437011,15.3702274 C11.3334611,15.3429484 11.1300986,15.2494762 10.9672623,15.0896537 L6.22355506,10.4337426 C5.83306468,10.0504793 5.82720591,9.42322812 6.21046913,9.03273774 C6.21262969,9.03053644 6.21480051,9.02834524 6.21698154,9.02616421 C6.60856962,8.63457613 7.2425402,8.63161543 7.63776862,9.019529 L11.6936353,13.00033 Z" id="path-1"></path>
</defs>
<g id="Left" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="mask" fill="#D6D9DB" transform="translate(11.735534, 12.054591) rotate(-270.000000) translate(-11.735534, -12.054591) " xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)">
<g transform="translate(12.000000, 12.000000) rotate(-90.000000) translate(-12.000000, -12.000000) "></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Loader</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M10,1 C10,0.44771525 10.4477153,0 11,0 C11.5522847,0 12,0.44771525 12,1 L12,5 C12,5.55228475 11.5522847,6 11,6 C10.4477153,6 10,5.55228475 10,5 L10,1 Z M10,17 C10,16.4477153 10.4477153,16 11,16 C11.5522847,16 12,16.4477153 12,17 L12,21 C12,21.5522847 11.5522847,22 11,22 C10.4477153,22 10,21.5522847 10,21 L10,17 Z M3.22289322,4.63710678 C2.83236893,4.24658249 2.83236893,3.61341751 3.22289322,3.22289322 C3.61341751,2.83236893 4.24658249,2.83236893 4.63710678,3.22289322 L7.46710678,6.05289322 C7.85763107,6.44341751 7.85763107,7.07658249 7.46710678,7.46710678 C7.07658249,7.85763107 6.44341751,7.85763107 6.05289322,7.46710678 L3.22289322,4.63710678 Z M14.5328932,15.9471068 C14.1423689,15.5565825 14.1423689,14.9234175 14.5328932,14.5328932 C14.9234175,14.1423689 15.5565825,14.1423689 15.9471068,14.5328932 L18.7771068,17.3628932 C19.1676311,17.7534175 19.1676311,18.3865825 18.7771068,18.7771068 C18.3865825,19.1676311 17.7534175,19.1676311 17.3628932,18.7771068 L14.5328932,15.9471068 Z M1,12 C0.44771525,12 0,11.5522847 0,11 C0,10.4477153 0.44771525,10 1,10 L5,10 C5.55228475,10 6,10.4477153 6,11 C6,11.5522847 5.55228475,12 5,12 L1,12 Z M17,12 C16.4477153,12 16,11.5522847 16,11 C16,10.4477153 16.4477153,10 17,10 L21,10 C21.5522847,10 22,10.4477153 22,11 C22,11.5522847 21.5522847,12 21,12 L17,12 Z M4.63710678,18.7771068 C4.24658249,19.1676311 3.61341751,19.1676311 3.22289322,18.7771068 C2.83236893,18.3865825 2.83236893,17.7534175 3.22289322,17.3628932 L6.05289322,14.5328932 C6.44341751,14.1423689 7.07658249,14.1423689 7.46710678,14.5328932 C7.85763107,14.9234175 7.85763107,15.5565825 7.46710678,15.9471068 L4.63710678,18.7771068 Z M15.9471068,7.46710678 C15.5565825,7.85763107 14.9234175,7.85763107 14.5328932,7.46710678 C14.1423689,7.07658249 14.1423689,6.44341751 14.5328932,6.05289322 L17.3628932,3.22289322 C17.7534175,2.83236893 18.3865825,2.83236893 18.7771068,3.22289322 C19.1676311,3.61341751 19.1676311,4.24658249 18.7771068,4.63710678 L15.9471068,7.46710678 Z" id="path-1"></path>
</defs>
<g id="Loader" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="loader" transform="translate(1.000000, 1.000000)">
<g id="Shape-+-Shape-+-Shape-+-Shape-+-Shape-+-Shape-+-Shape-Mask">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)">
<g transform="translate(-1.000000, -1.000000)"></g>
</g>
<g mask="url(#mask-2)" fill="#000000" fill-rule="nonzero" stroke="#FFFFFF" stroke-width="2">
<path d="M10,1 C10,0.44771525 10.4477153,0 11,0 C11.5522847,0 12,0.44771525 12,1 L12,5 C12,5.55228475 11.5522847,6 11,6 C10.4477153,6 10,5.55228475 10,5 L10,1 Z" id="Mask" opacity="0.200000003"></path>
<path d="M10,17 C10,16.4477153 10.4477153,16 11,16 C11.5522847,16 12,16.4477153 12,17 L12,21 C12,21.5522847 11.5522847,22 11,22 C10.4477153,22 10,21.5522847 10,21 L10,17 Z" id="Shape" opacity="0.600000024"></path>
<path d="M3.22289322,4.63710678 C2.83236893,4.24658249 2.83236893,3.61341751 3.22289322,3.22289322 C3.61341751,2.83236893 4.24658249,2.83236893 4.63710678,3.22289322 L7.46710678,6.05289322 C7.85763107,6.44341751 7.85763107,7.07658249 7.46710678,7.46710678 C7.07658249,7.85763107 6.44341751,7.85763107 6.05289322,7.46710678 L3.22289322,4.63710678 Z" id="Shape" opacity="0.300000012"></path>
<path d="M14.5328932,15.9471068 C14.1423689,15.5565825 14.1423689,14.9234175 14.5328932,14.5328932 C14.9234175,14.1423689 15.5565825,14.1423689 15.9471068,14.5328932 L18.7771068,17.3628932 C19.1676311,17.7534175 19.1676311,18.3865825 18.7771068,18.7771068 C18.3865825,19.1676311 17.7534175,19.1676311 17.3628932,18.7771068 L14.5328932,15.9471068 Z" id="Shape" opacity="0.699999988"></path>
<path d="M1,12 C0.44771525,12 0,11.5522847 0,11 C0,10.4477153 0.44771525,10 1,10 L5,10 C5.55228475,10 6,10.4477153 6,11 C6,11.5522847 5.55228475,12 5,12 L1,12 Z" id="Shape" opacity="0.400000006"></path>
<path d="M17,12 C16.4477153,12 16,11.5522847 16,11 C16,10.4477153 16.4477153,10 17,10 L21,10 C21.5522847,10 22,10.4477153 22,11 C22,11.5522847 21.5522847,12 21,12 L17,12 Z" id="Shape" opacity="0.800000012"></path>
<path d="M4.63710678,18.7771068 C4.24658249,19.1676311 3.61341751,19.1676311 3.22289322,18.7771068 C2.83236893,18.3865825 2.83236893,17.7534175 3.22289322,17.3628932 L6.05289322,14.5328932 C6.44341751,14.1423689 7.07658249,14.1423689 7.46710678,14.5328932 C7.85763107,14.9234175 7.85763107,15.5565825 7.46710678,15.9471068 L4.63710678,18.7771068 Z" id="Shape" opacity="0.5"></path>
<path d="M15.9471068,7.46710678 C15.5565825,7.85763107 14.9234175,7.85763107 14.5328932,7.46710678 C14.1423689,7.07658249 14.1423689,6.44341751 14.5328932,6.05289322 L17.3628932,3.22289322 C17.7534175,2.83236893 18.3865825,2.83236893 18.7771068,3.22289322 C19.1676311,3.61341751 19.1676311,4.24658249 18.7771068,4.63710678 L15.9471068,7.46710678 Z" id="Shape" opacity="0.100000001"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Metric</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M14.1111111,15.8377223 L15.4957611,11.6837722 C15.6318752,11.2754301 16.014014,11 16.4444444,11 L19.5555556,11 C20.1078403,11 20.5555556,11.4477153 20.5555556,12 C20.5555556,12.5522847 20.1078403,13 19.5555556,13 L17.1652037,13 L15.0597944,19.3162278 C14.7558956,20.2279241 13.4663266,20.2279241 13.1624278,19.3162278 L9.44444444,8.16227766 L8.05979441,12.3162278 C7.92368036,12.7245699 7.54154152,13 7.11111111,13 L4,13 C3.44771525,13 3,12.5522847 3,12 C3,11.4477153 3.44771525,11 4,11 L6.39035189,11 L8.49576115,4.68377223 C8.79965992,3.77207592 10.089229,3.77207592 10.3931277,4.68377223 L14.1111111,15.8377223 Z" id="path-1"></path>
</defs>
<g id="Metric" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Minimize</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M5,2 L19,2 C20.6568542,2 22,3.34314575 22,5 L22,19 C22,20.6568542 20.6568542,22 19,22 L5,22 C3.34314575,22 2,20.6568542 2,19 L2,5 C2,3.34314575 3.34314575,2 5,2 Z M5,4 C4.44771525,4 4,4.44771525 4,5 L4,19 C4,19.5522847 4.44771525,20 5,20 L19,20 C19.5522847,20 20,19.5522847 20,19 L20,5 C20,4.44771525 19.5522847,4 19,4 L5,4 Z M8,13 C7.44771525,13 7,12.5522847 7,12 C7,11.4477153 7.44771525,11 8,11 L16,11 C16.5522847,11 17,11.4477153 17,12 C17,12.5522847 16.5522847,13 16,13 L8,13 Z" id="path-1"></path>
</defs>
<g id="Minimize" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>More</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12,14 C10.8954305,14 10,13.1045695 10,12 C10,10.8954305 10.8954305,10 12,10 C13.1045695,10 14,10.8954305 14,12 C14,13.1045695 13.1045695,14 12,14 Z M12,7 C10.8954305,7 10,6.1045695 10,5 C10,3.8954305 10.8954305,3 12,3 C13.1045695,3 14,3.8954305 14,5 C14,6.1045695 13.1045695,7 12,7 Z M12,21 C10.8954305,21 10,20.1045695 10,19 C10,17.8954305 10.8954305,17 12,17 C13.1045695,17 14,17.8954305 14,19 C14,20.1045695 13.1045695,21 12,21 Z" id="path-1"></path>
</defs>
<g id="More" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Null Value</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M3,11 L6,11 C6.55228475,11 7,11.4477153 7,12 C7,12.5522847 6.55228475,13 6,13 L3,13 C2.44771525,13 2,12.5522847 2,12 C2,11.4477153 2.44771525,11 3,11 Z M10,11 L13,11 C13.5522847,11 14,11.4477153 14,12 C14,12.5522847 13.5522847,13 13,13 L10,13 C9.44771525,13 9,12.5522847 9,12 C9,11.4477153 9.44771525,11 10,11 Z M17,11 L20,11 C20.5522847,11 21,11.4477153 21,12 C21,12.5522847 20.5522847,13 20,13 L17,13 C16.4477153,13 16,12.5522847 16,12 C16,11.4477153 16.4477153,11 17,11 Z" id="path-1"></path>
</defs>
<g id="Null-Value" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="lines" fill="#11111F" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Null-Value</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M3,11 L6,11 C6.55228475,11 7,11.4477153 7,12 C7,12.5522847 6.55228475,13 6,13 L3,13 C2.44771525,13 2,12.5522847 2,12 C2,11.4477153 2.44771525,11 3,11 Z M10,11 L13,11 C13.5522847,11 14,11.4477153 14,12 C14,12.5522847 13.5522847,13 13,13 L10,13 C9.44771525,13 9,12.5522847 9,12 C9,11.4477153 9.44771525,11 10,11 Z M17,11 L20,11 C20.5522847,11 21,11.4477153 21,12 C21,12.5522847 20.5522847,13 20,13 L17,13 C16.4477153,13 16,12.5522847 16,12 C16,11.4477153 16.4477153,11 17,11 Z" id="path-1"></path>
</defs>
<g id="Null-Value" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="lines" fill="#11111F" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Parameters</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M18,16 L17,16 C16.5167508,16 16.125,15.5522847 16.125,15 C16.125,14.4477153 16.5167508,14 17,14 L21.125,14 C21.6082492,14 22,14.4477153 22,15 C22,15.5522847 21.6082492,16 21.125,16 L20,16 L20,19.1428571 C20,19.6162441 19.5522847,20 19,20 C18.4477153,20 18,19.6162441 18,19.1428571 L18,16 Z M11,7 L11,3.85714286 C11,3.38375593 11.4477153,3 12,3 C12.5522847,3 13,3.38375593 13,3.85714286 L13,7 L14,7 C14.4832492,7 14.875,7.44771525 14.875,8 C14.875,8.55228475 14.4832492,9 14,9 L9.875,9 C9.39175084,9 9,8.55228475 9,8 C9,7.44771525 9.39175084,7 9.875,7 L11,7 Z M4,14 L3,14 C2.51675084,14 2.125,13.5522847 2.125,13 C2.125,12.4477153 2.51675084,12 3,12 L7.125,12 C7.60824916,12 8,12.4477153 8,13 C8,13.5522847 7.60824916,14 7.125,14 L6,14 L6,19.1111111 C6,19.6020309 5.55228475,20 5,20 C4.44771525,20 4,19.6020309 4,19.1111111 L4,14 Z M6,10.1111111 C6,10.6020309 5.55228475,11 5,11 C4.44771525,11 4,10.6020309 4,10.1111111 L4,3.88888889 C4,3.39796911 4.44771525,3 5,3 C5.55228475,3 6,3.39796911 6,3.88888889 L6,10.1111111 Z M13,19.1818182 C13,19.6336875 12.5522847,20 12,20 C11.4477153,20 11,19.6336875 11,19.1818182 L11,11.8181818 C11,11.3663125 11.4477153,11 12,11 C12.5522847,11 13,11.3663125 13,11.8181818 L13,19.1818182 Z M20,11.1818182 C20,11.6336875 19.5522847,12 19,12 C18.4477153,12 18,11.6336875 18,11.1818182 L18,3.81818182 C18,3.36631248 18.4477153,3 19,3 C19.5522847,3 20,3.36631248 20,3.81818182 L20,11.1818182 Z" id="path-1"></path>
</defs>
<g id="Parameters" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)">
<g transform="translate(2.000000, 2.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Person</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M21,21 C21,21.5522847 20.5522847,22 20,22 C19.4477153,22 19,21.5522847 19,21 L19,19 C19,17.3431458 17.6568542,16 16,16 L8,16 C6.34314575,16 5,17.3431458 5,19 L5,21 C5,21.5522847 4.55228475,22 4,22 C3.44771525,22 3,21.5522847 3,21 L3,19 C3,16.2385763 5.23857625,14 8,14 L16,14 C18.7614237,14 21,16.2385763 21,19 L21,21 Z M12,12 C9.23857625,12 7,9.76142375 7,7 C7,4.23857625 9.23857625,2 12,2 C14.7614237,2 17,4.23857625 17,7 C17,9.76142375 14.7614237,12 12,12 Z M12,10 C13.6568542,10 15,8.65685425 15,7 C15,5.34314575 13.6568542,4 12,4 C10.3431458,4 9,5.34314575 9,7 C9,8.65685425 10.3431458,10 12,10 Z" id="path-1"></path>
</defs>
<g id="Person" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#11111F" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Preview-Fillled</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M2.12210443,11.5211479 C2.24177844,11.3017455 2.46286377,10.9414583 2.78336984,10.4865465 C3.31200459,9.73622618 3.93535882,8.98683858 4.6531551,8.28499333 C6.74195958,6.24260673 9.19893197,5 12,5 C14.801068,5 17.2580404,6.24260673 19.3468449,8.28499333 C20.0646412,8.98683858 20.6879954,9.73622618 21.2166302,10.4865465 C21.5371362,10.9414583 21.7582216,11.3017455 21.8778956,11.5211479 C22.0407015,11.8196254 22.0407015,12.1803746 21.8778956,12.4788521 C21.7582216,12.6982545 21.5371362,13.0585417 21.2166302,13.5134535 C20.6879954,14.2637738 20.0646412,15.0131614 19.3468449,15.7150067 C17.2580404,17.7573933 14.801068,19 12,19 C9.19893197,19 6.74195958,17.7573933 4.6531551,15.7150067 C3.93535882,15.0131614 3.31200459,14.2637738 2.78336984,13.5134535 C2.46286377,13.0585417 2.24177844,12.6982545 2.12210443,12.4788521 C1.95929852,12.1803746 1.95929852,11.8196254 2.12210443,11.5211479 Z M12,15 C13.6568542,15 15,13.6568542 15,12 C15,10.3431458 13.6568542,9 12,9 C10.3431458,9 9,10.3431458 9,12 C9,13.6568542 10.3431458,15 12,15 Z M12,13 C11.4477153,13 11,12.5522847 11,12 C11,11.4477153 11.4477153,11 12,11 C12.5522847,11 13,11.4477153 13,12 C13,12.5522847 12.5522847,13 12,13 Z" id="path-1"></path>
</defs>
<g id="Preview-Fillled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="eye" mask="url(#mask-2)">
<g transform="translate(3.000000, 6.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Preview</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M4.41833471,12.3615465 C4.880467,13.0174762 5.42685709,13.6743386 6.05139036,14.2849933 C7.79781315,15.9926067 9.78970439,17 12,17 C14.2102956,17 16.2021868,15.9926067 17.9486096,14.2849933 C18.5731429,13.6743386 19.119533,13.0174762 19.5816653,12.3615465 C19.6745267,12.2297432 19.7567783,12.1086112 19.8282486,12 C19.7567783,11.8913888 19.6745267,11.7702568 19.5816653,11.6384535 C19.119533,10.9825238 18.5731429,10.3256614 17.9486096,9.71500667 C16.2021868,8.00739327 14.2102956,7 12,7 C9.78970439,7 7.79781315,8.00739327 6.05139036,9.71500667 C5.42685709,10.3256614 4.880467,10.9825238 4.41833471,11.6384535 C4.32547332,11.7702568 4.24322174,11.8913888 4.17175138,12 C4.24322174,12.1086112 4.32547332,12.2297432 4.41833471,12.3615465 Z M2.12210443,11.5211479 C2.24177844,11.3017455 2.46286377,10.9414583 2.78336984,10.4865465 C3.31200459,9.73622618 3.93535882,8.98683858 4.6531551,8.28499333 C6.74195958,6.24260673 9.19893197,5 12,5 C14.801068,5 17.2580404,6.24260673 19.3468449,8.28499333 C20.0646412,8.98683858 20.6879954,9.73622618 21.2166302,10.4865465 C21.5371362,10.9414583 21.7582216,11.3017455 21.8778956,11.5211479 C22.0407015,11.8196254 22.0407015,12.1803746 21.8778956,12.4788521 C21.7582216,12.6982545 21.5371362,13.0585417 21.2166302,13.5134535 C20.6879954,14.2637738 20.0646412,15.0131614 19.3468449,15.7150067 C17.2580404,17.7573933 14.801068,19 12,19 C9.19893197,19 6.74195958,17.7573933 4.6531551,15.7150067 C3.93535882,15.0131614 3.31200459,14.2637738 2.78336984,13.5134535 C2.46286377,13.0585417 2.24177844,12.6982545 2.12210443,12.4788521 C1.95929852,12.1803746 1.95929852,11.8196254 2.12210443,11.5211479 Z M12,15 C13.6568542,15 15,13.6568542 15,12 C15,10.3431458 13.6568542,9 12,9 C10.3431458,9 9,10.3431458 9,12 C9,13.6568542 10.3431458,15 12,15 Z M12,13 C11.4477153,13 11,12.5522847 11,12 C11,11.4477153 11.4477153,11 12,11 C12.5522847,11 13,11.4477153 13,12 C13,12.5522847 12.5522847,13 12,13 Z" id="path-1"></path>
</defs>
<g id="Preview" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="eye" mask="url(#mask-2)">
<g transform="translate(3.000000, 6.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Right</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11.8360258,12.9683738 L15.8850885,9.07112198 C16.2852079,8.68600418 16.9199893,8.69206724 17.3126795,9.08475748 C17.6958143,9.46789224 17.6958143,10.0890764 17.3126795,10.4722112 C17.3082623,10.4766285 17.3038029,10.4810034 17.2993021,10.4853355 L12.5692028,15.03809 C12.3885947,15.2119265 12.1601745,15.3060604 11.9277889,15.3209105 C11.6419463,15.3484786 11.3464713,15.2567449 11.1220462,15.044577 L6.29254816,10.4788485 C5.90232724,10.10994 5.8850501,9.49454399 6.25395859,9.10432306 C6.26020808,9.09771253 6.26655004,9.09119003 6.27298258,9.08475748 C6.66667636,8.69106371 7.30217448,8.68214471 7.70676173,9.06463489 L11.8360258,12.9683738 Z" id="path-1"></path>
</defs>
<g id="Right" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="mask" fill="#D6D9DB" transform="translate(11.794113, 12.054606) rotate(-90.000000) translate(-11.794113, -12.054606) " xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)">
<g transform="translate(12.000000, 12.000000) rotate(-90.000000) translate(-12.000000, -12.000000) "></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Search</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M16.6176858,18.0318994 C15.078015,19.2635271 13.1250137,20 11,20 C6.02943725,20 2,15.9705627 2,11 C2,6.02943725 6.02943725,2 11,2 C15.9705627,2 20,6.02943725 20,11 C20,13.1250137 19.2635271,15.078015 18.0318994,16.6176858 L21.7071068,20.2928932 C22.0976311,20.6834175 22.0976311,21.3165825 21.7071068,21.7071068 C21.3165825,22.0976311 20.6834175,22.0976311 20.2928932,21.7071068 L16.6176858,18.0318994 Z M11,18 C14.8659932,18 18,14.8659932 18,11 C18,7.13400675 14.8659932,4 11,4 C7.13400675,4 4,7.13400675 4,11 C4,14.8659932 7.13400675,18 11,18 Z" id="path-1"></path>
</defs>
<g id="Search" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#11111F" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Speech</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M4,18.5857864 L6.29289322,16.2928932 C6.4804296,16.1053568 6.73478351,16 7,16 L19,16 C19.5522847,16 20,15.5522847 20,15 L20,5 C20,4.44771525 19.5522847,4 19,4 L5,4 C4.44771525,4 4,4.44771525 4,5 L4,18.5857864 Z M7.41421356,18 L3.70710678,21.7071068 C3.07714192,22.3370716 2,21.8909049 2,21 L2,5 C2,3.34314575 3.34314575,2 5,2 L19,2 C20.6568542,2 22,3.34314575 22,5 L22,15 C22,16.6568542 20.6568542,18 19,18 L7.41421356,18 Z M8,11 C8.55228475,11 9,10.5522847 9,10 C9,9.44771525 8.55228475,9 8,9 C7.44771525,9 7,9.44771525 7,10 C7,10.5522847 7.44771525,11 8,11 Z M12,11 C12.5522847,11 13,10.5522847 13,10 C13,9.44771525 12.5522847,9 12,9 C11.4477153,9 11,9.44771525 11,10 C11,10.5522847 11.4477153,11 12,11 Z M16,11 C16.5522847,11 17,10.5522847 17,10 C17,9.44771525 16.5522847,9 16,9 C15.4477153,9 15,9.44771525 15,10 C15,10.5522847 15.4477153,11 16,11 Z" id="path-1"></path>
</defs>
<g id="Speech" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Shape" fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Up-Arrow</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M11,7.92054674 L7.54660817,10.6832602 C7.16925397,10.9851436 6.61862315,10.9239624 6.31673979,10.5466082 C6.01485644,10.169254 6.07603764,9.61862315 6.45339183,9.31673979 L11.4533918,5.31673979 C11.7729582,5.06108674 12.2270418,5.06108674 12.5466082,5.31673979 L17.5466082,9.31673979 C17.9239624,9.61862315 17.9851436,10.169254 17.6832602,10.5466082 C17.3813769,10.9239624 16.830746,10.9851436 16.4533918,10.6832602 L13,7.92054674 L13,17 C13,17.6642136 12.5522847,18 12,18 C11.4477153,18 11,17.6642136 11,17 L11,7.92054674 Z" id="path-1"></path>
</defs>
<g id="Up-Arrow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Combined-Shape" fill="#000000" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="Tint/Black" mask="url(#mask-2)"></g>
<g id="triangle" mask="url(#mask-2)">
<g transform="translate(1.000000, 2.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Up</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M12.0965192,12.1196553 L16.1455819,8.22240346 C16.5457013,7.83728567 17.1804827,7.84334872 17.573173,8.23603897 C17.9563077,8.61917373 17.9563077,9.2403579 17.573173,9.62349266 C17.5687557,9.62790994 17.5642963,9.63228493 17.5597955,9.63661703 L12.8296962,14.1893714 C12.6490881,14.363208 12.420668,14.4573419 12.1882823,14.472192 C11.9024397,14.4997601 11.6069647,14.4080264 11.3825396,14.1958585 L6.55304158,9.63012994 C6.16282066,9.26122145 6.14554352,8.64582547 6.51445201,8.25560455 C6.5207015,8.24899402 6.52704345,8.24247151 6.533476,8.23603897 C6.92716977,7.84234519 7.56266789,7.8334262 7.96725514,8.21591638 L12.0965192,12.1196553 Z" id="path-1"></path>
</defs>
<g id="Up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="mask" fill="#D6D9DB" transform="translate(12.054606, 11.205887) rotate(-180.000000) translate(-12.054606, -11.205887) " xlink:href="#path-1"></use>
<g id="Tint/Gray-3" mask="url(#mask-2)">
<g transform="translate(12.000000, 12.000000) rotate(-180.000000) translate(-12.000000, -12.000000) translate(0.000000, 0.000000)"></g>
</g>
</g>
</svg>
\ No newline at end of file
module.exports = {
roots: [
'<rootDir>/js',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(j|t)sx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
'json',
],
globals: {
'ts-jest': {
diagnostics: false,
},
},
};
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
// TODO - Consider an alternative to react-sanitized-html (large filesize)
import SanitizedHTML from 'react-sanitized-html';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
import { AnnouncementsGetRequest } from "../../ducks/announcements/reducer";
import { AnnouncementPost } from "./types";
interface AnnouncementPageState {
posts: AnnouncementPost[];
}
export interface StateFromProps {
posts: AnnouncementPost[];
}
export interface DispatchFromProps {
announcementsGet: () => AnnouncementsGetRequest;
}
type AnnouncementPageProps = StateFromProps & DispatchFromProps;
class AnnouncementPage extends React.Component<AnnouncementPageProps, AnnouncementPageState> {
constructor(props) {
super(props);
this.state = {
posts: this.props.posts,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
const { posts } = nextProps;
return { posts };
}
componentDidMount() {
this.props.announcementsGet();
}
createPost(post: AnnouncementPost, postIndex: number) {
return (
<div key={`post:${postIndex}`} className='post-container'>
<div className='post-header'>
<div className='post-title'>{post.title}</div>
<div className='post-date'>{post.date}</div>
</div>
<div className='post-content'>
<SanitizedHTML html={post.html_content} />
</div>
</div>
);
}
render() {
return (
<DocumentTitle title="Announcements - Amundsen">
<div className="container announcement-container">
<div className="row">
<div className="col-xs-12">
<div className="announcement-header">
Announcements
</div>
<hr />
<div className='announcement-content'>
{
this.state.posts.map((post, index) => {
return this.createPost(post, index)
})
}
</div>
</div>
</div>
</div>
</DocumentTitle>
);
}
}
export default AnnouncementPage;
@import 'variables';
.announcement-container {
margin: 64px auto 48px;
}
.announcement-container hr {
border: 2px solid $gradient-4;
}
.announcement-header {
font-size: 20px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
}
.post-container {
display: flex;
flex-direction: row;
margin-bottom: 32px;
}
.post-header {
padding-right: 16px;
border-right: 1px solid $gray-lighter
}
.post-title {
font-size: 16px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
width: 150px;
}
.post-date {
font-size: 14px;
color: $gray-light;
}
.post-content {
padding-left: 16px;
}
.post-content > p:not(:last-child) {
margin: 0 0 20px;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment