1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 00:38:06 +00:00
This commit is contained in:
checktheroads 2019-05-07 23:21:41 -07:00
commit 47f1b938cd
125 changed files with 78836 additions and 0 deletions

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
test.py
.DS_Store
*.sass-cache
hyperglass/config/*.toml
hyperglass/.flask_cache/
#
# Github Default from https://github.com/martinohanlon/flightlight/issues/1
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
assets/

32
LICENSE Normal file
View file

@ -0,0 +1,32 @@
The Clear BSD License
Copyright (c) 2019 Matthew Love
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

46
README.md Normal file
View file

@ -0,0 +1,46 @@
<img src="hyperglass/static/images/hyperglass-dark.png" width=300></img>
**Hyperglass** is a network looking glass application. A looking glass is typically implemented by network service providers as a way of providing customers, peers, and partners with a way to easily view elements of, or run tests from the provider's network.
<br>
![GitHub issues](https://img.shields.io/github/issues/checktheroads/hyperglass.svg)
![GitHub](https://img.shields.io/github/license/checktheroads/hyperglass.svg)
![GitHub top language](https://img.shields.io/github/languages/top/checktheroads/hyperglass.svg)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
## Features
- BGP Route, BGP Community, BGP AS_PATH, Ping, Traceroute
- Full IPv6 support
- [Netmiko](https://github.com/ktbyers/netmiko)-based connection handling
- Customizable commands for each function by vendor
- Clean, google-esq GUI based on the [Bumla](https://bulma.io) framework
- Customizable colors, logo, web fonts, error messages, UI text
- TOML-based config file for all customizable parameters (no databases!)
- Configurable IP/Prefix "blacklist" to prevent lookup of internal/private prefixes
- Configurable rate limiting, powered by [Flask-Limiter](https://github.com/alisaifee/flask-limiter)
- Query response caching with configurable cache timeout, powered by [Flask-Caching](https://github.com/sh4nks/flask-caching)
## Documentation
Documentation can be found [here](https://hyperglass.readthedocs.io), or in the `docs/` directory.
## Preview
## Platform Support
Theoretically, any vendor supported by Netmiko can be supported by Hyperglass. However, I am currently listing platforms I have personally tested and verified full functionality with:
### Routers
- Cisco IOS-XR: `cisco_xr`
- Cisco Classic IOS/IOS-XE: `cisco_ios`
- Juniper JunOS: `junos`
### Proxies
- Linux: `linux_ssh`
## Acknowledgements
- This project originally started as a fork of vraulsan's [looking-glass](https://github.com/vraulsan/looking-glass) project. The guts of the Flask components still remain from that project, but almost everything else has been rewritten. Nevertheless, the inspiration for building hyperglass came from here.
## License
[Clear BSD License](https://github.com/checktheroads/hyperglass/master/LICENSE)

BIN
docs/blacklist_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

7
docs/caching.md Normal file
View file

@ -0,0 +1,7 @@
Hyperglass supports caching the application's responses for a configurable period of time to reduce the number of lookups passed back to routers for repetitive/common lookups. By default, all application responses (including error messages), are cached for 2 minutes in the local file system.
Hyperglass uses [Flask-Caching](https://github.com/sh4nks/flask-caching), which is able to use a wide variety of cache storage backends (Filesystem, Redis, Memcached, etc.). By default, Hyperglass uses the Filesystem method, and stores the cached data in `hyperglass/hyperglass/.flask_cache`.
The cache list little more than a key value store. Hyperglass uses a stringified combination of router/location name, command used, and query type as a key, and the output as a value. If a lookup matching the exact key is found, the cached value will be returned. If not, a standard lookup is performed (and that data is subsequently cached).
# Insert Video Here

18
docs/configuration.md Normal file
View file

@ -0,0 +1,18 @@
# Configuration
Hyperglass configuration files are stored `hyperglass/hyperglass/config`, in [TOML](https://github.com/toml-lang/toml) format.
Example configuration files are provided and end in `.example`. All example configuration files should be copied to their `.toml` name & extension. For example:
```console
$ cd hyperglass/hyperglass/config
$
$ cp blacklist.toml.example blacklist.toml
$ cp commands.toml.example commands.toml
$ cp config.toml.example config.toml
$ cp devices.toml.example devices.toml
```
## `requires_ipv6_cidr.toml`
Some platforms (namely Cisco IOS) are unable to perform a BGP lookup by IPv6 host address (e.g. 2001:db8::1), but must perform the lookup by prefix (e.g. 2001:db8::/48). `requires_ipv6_cidr.toml` is a list (TOML array) of network operating systems that require this (in Netmiko format).

View file

@ -0,0 +1,16 @@
Authentication parameters are stored in the `devices.toml` file, at `hyperglass/hyperglass/config/devices.toml`. The array of tables simply stores the username and password for a device. SSH Key authentication is not yet supported.
Example:
```toml
[credential.'default']
username = "hyperglass"
password = "secret_password"
[credential.'other_credential']
username = "other_username"
password = "other_secret_password"
```
!!! warning "Security Warning"
These values are stored in plain text. Make sure the accounts are restricted and that the configuration file is stored in a secure location.

View file

@ -0,0 +1,25 @@
Blacklisted querys are defined in `hyperglass/hyperglass/config/blacklist.toml`.
The blacklist is a simple TOML array (list) of host IPs or prefixes that you do not want end users to be able to query. For example, if you want to prevent users from looking up 198.18.0.0/15 or any contained host or prefix, you can add it to the blacklist:
```toml
blacklist = [
198.18.0.0/15
]
```
If you have multiple hosts/subnets you wish to blacklist, you can do so by adding a comma `,` after each entry (except the last):
```toml
blacklist = [
'198.18.0.0/15',
'10.0.0.0/8',
'192.168.0.0/16',
'2001:db8::/32'
'172.16.0.0/12'
]
```
When users attempt to query a matching host/prefix, they will receive the following error message by default:
<img src="/blacklist_error.png"></img>

View file

@ -0,0 +1,306 @@
<style>
.bd-color {
border-radius: 1px;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.1), inset 0 0 0 1px rgba(0,0,0,.1);
display: inline-block;
float: left;
height: 16px;
margin-right: 2px;
width: 16px;
}
</style>
From `hyperglass/hyperglass/config/config.toml`:
### site_title
| Type | Default Value |
| ------ | -------------- |
| String | `"hyperglass"` |
HTML `<title>` element that is shown in a browser's title bar.
### title_mode
| Type | Default Value |
| ------ | ------------- |
| String | `"none"` |
Controls the title section on the main page.
#### Parameters
##### `"none"`
Hides Title and Subtitle text, displays logo defined in [logo_path](#logo_path).
##### `"both"`
Displays both Title and Subtitle text defined in [title](#title) and [subtitle](#subtitle) parameters.
##### `"hide_subtitle"`
Displays only the Title text defined in the [title](#title) parameter.
### title
| Type | Default Value |
| ------ | -------------- |
| String | `"hyperglass"` |
### subtitle
| Type | Default Value |
| ------ | -------------------- |
| String | `"AS" + primary_asn` |
See [primary_asn](#primary_asn) parameter.
### enable_footer
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables entire footer element, which contains text defined in `hyperglass/hyperglass/templates/footer.md`.
### enable_credit
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables hoverable icon on the left side of the footer, which links to the hyperglass repo.
### color_btn_submit
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the submit button.
### color_tag_loctitle
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Sets color of the title portion of the location tag which appears at the top of the results box on the left side.
### color_tag_cmdtitle
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#330036"` | <span class="bd-color" style="background-color: #330036;"></span> |
Sets color of the title portion of the command tag which appears at the top of the results box on the right side.
### color_tag_cmd
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#ff5e5b"` | <span class="bd-color" style="background-color: #ff5e5b;"></span> |
Sets color of the command name portion of the command tag which appears at the top of the results box on the right side.
### color_tag_loc
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the location name portion of the location tag which appears at the top of the results box on the left side.
### color_hero
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#fbfffe"` | <span class="bd-color" style="background-color: #fbfffe;"></span> |
Sets the background color of the main page. The main page is a Bulma [fullheight hero class](https://bulma.io/documentation/layout/hero/) layout. This parameter will set the color of the entire hero `<section>` class, including navbar, head, body, and footer subclasses.
### color_progressbar
| Type | Default Value | Preview |
| ------ | ------------- | ----------------------------------------------------------------- |
| String | `"#40798c"` | <span class="bd-color" style="background-color: #40798c;"></span> |
Sets color of the progress bar that displays while the back-end application processes the request.
### logo_path
| Type | Default Value |
| ------ | ------------------------------------- |
| String | `"static/images/hyperglass-dark.png"` |
Sets the path to the logo file, which will be displayed if [title_mode](#title_mode) is set to `"logo_only"`. This file can be any browser-compatible format, such as JPEG, PNG, or SVG.
### logo_width
| Type | Default Value |
| ------ | ------------- |
| String | `"384"` |
Sets the width of the logo defined in the [logo_path](#logo_path) parameter. This is helpful if your logo is a dimension that doesn't quite work with the default width.
### placeholder_prefix
| Type | Default Value |
| ------ | ------------------------------------- |
| String | `"Prefix, IP, Community, or AS_PATH"` |
Sets the placeholder text that appears in the main search box.
### show_peeringdb
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the PeeringDB link in the upper right corner. If `True`, the [primary_asn](#primary_asn) will be automatically used to create the URL to your ASN's PeeringDB entry.
### text_results
| Type | Default Value |
| ------ | ------------- |
| String | `"Results"` |
Sets the header text of the results box.
### text_location
| Type | Default Value |
| ------ | ------------- |
| String | `"Location"` |
Sets the placeholder text of the location selector.
### text_cache
| Type | Default Value |
| ------ | ------------------------------------------------------- |
| String | `"Results will be cached for {cache_timeout} minutes."` |
Sets the text at the bottom of the results box that states the cache timeout. `{cache_timeout}` will be formatted with the value of [cache_timeout](/configuration/general/#cache_timeout).
### text_limiter_title
| Type | Default Value |
| ------ | ----------------- |
| String | `"Limit Reached"` |
Sets the title text for the site-wide rate limit page. Users are redirected to this page when they have accessed the site more than the [specified](/configuration/general/#rate_limit_site) limit.
### text_limiter_subtitle
| Type | Default Value |
| ------ | ------------------------------------------------------------------------------------- |
| String | `"You have accessed this site more than {rate_limit_site} times in the last minute."` |
Sets the subtitle text for the site-wide rate limit page. Users are redirected to this page when they have accessed the site more than the [specified](/configuration/general/#rate_limit_site) limit. `{rate_limit_site}` will be formatted with the value of [rate_limit_site](/configuration/general/#rate_limit_site).
### text_415_title
| Type | Default Value |
| ------ | ----------------- |
| String | `"Error"` |
Sets the title text for the full general error page.
### text_415_subtitle
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Something went wrong."` |
Sets the subtitle text for the full general error page.
### text_415_button
| Type | Default Value |
| ------ | ----------------- |
| String | `"Home"` |
Sets the button text for the full general error page.
### text_help_bgp_route
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Performs BGP table lookup based on IPv4/IPv6 prefix."` |
Sets the BGP Route query help text, displayed when the **?** icon is hovered.
### text_help_bgp_community
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs BGP table lookup based on <a href="https://tools.ietf.org/html/rfc4360">Extended</a> or <a href="https://tools.ietf.org/html/rfc8195">Large</a> community value.'` |
Sets the BGP Community query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
### text_help_bgp_aspath
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs BGP table lookup based on <code>AS_PATH</code> regular expression.<br>For commonly used BGP regular expressions, <a href="https://hyperglass.readthedocs.io/en/latest/Extras/common_as_path_regex/">click here</a>.'` |
Sets the BGP AS Path query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
### text_help_ping
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Sends 5 ICMP echo requests to the target."` |
Sets the Ping query help text, displayed when the **?** icon is hovered.
### text_help_traceroute
| Type | Default Value |
| ------ | ------------------------- |
| String | `'Performs UDP Based traceroute to the target.<br>For information about how to interpret traceroute results, <a href="https://www.nanog.org/meetings/nanog45/presentations/Sunday/RAS_traceroute_N45.pdf">click here</a>.'` |
Sets the Traceroute query help text, displayed when the **?** icon is hovered.
!!! note
Since there are double quotes (`" "`) in the `<a>` HTML tags, single quotes (`' '`) are required for the TOML string.
### primary_font_url
| Type | Default Value |
| ------ | ------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Nunito:400,600,700"` |
Sets the web font URL for the primary font. This font is used for all titles, subtitles, and non-code/preformatted text. The value is passed as a Jinja2 variable to the head block in the base template.
### primary_font_name
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Nunito"` |
Sets the web font name for the primary font. This font is used for all titles, subtitles, and non-code/preformatted text. The value is passed as a Jinja2 variable to generate `hyperglass/hyperglass/static/sass/hyperglass.scss`, which ultimately get passed to CSS.
### mono_font_url
| Type | Default Value |
| ------ | ------------------------- |
| String | `"https://fonts.googleapis.com/css?family=Fira+Mono"` |
Sets the web font URL for the monospace/code/preformatted text font. This font is used for all query output text, as well as the command title and command name tag. The value is passed as a Jinja2 variable to the head block in the base template.
### mono_font_name
| Type | Default Value |
| ------ | ------------------------- |
| String | `"Fira Mono"` |
Sets the web font URL for the monospace/code/preformatted text font. This font is used for all query output text, as well as the command title and command name tag. The value is passed as a Jinja2 variable to generate `hyperglass/hyperglass/static/sass/hyperglass.scss`, which ultimately get passed to CSS.

View file

@ -0,0 +1,83 @@
Commands are defined in `hyperglass/hyperglass/config/commands.toml`. Formatted as a nested array of tables, each table defines the commands that will be used to execute the queries on the routers.
Each table contains three nested tables:
##### dual
Commands that are IP protocol agnostic:
- `bgp_community`
- `bgp_aspath`
##### ipv4
Commands that are IPv4-specific:
- `bgp_route`
- `ping`
- `traceroute`
##### ipv6
Commands that are IPv6-specific:
- `bgp_route`
- `ping`
- `traceroute`
#### Default Configuration
```toml
[[cisco_ios]]
[cisco_ios.dual]
bgp_community = "show bgp all community {target}"
bgp_aspath = 'show bgp all quote-regexp "{target}"'
[cisco_ios.ipv4]
bgp_route = "show bgp ipv4 unicast {target} | exclude pathid:|Epoch"
ping = "ping {target} repeat 5 source {src_addr_ipv4}"
traceroute = "traceroute {target} timeout 1 probe 2 source {src_addr_ipv4}"
[cisco_ios.ipv6]
bgp_route = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch"
ping = "ping ipv6 {target} repeat 5 source {src_addr_ipv6}"
traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
[[cisco_xr]]
[cisco_xr.dual]
bgp_community = 'show bgp all unicast community {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
bgp_aspath = 'show bgp all unicast regexp {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
[cisco_xr.ipv4]
bgp_route = 'show bgp ipv4 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
ping = "ping ipv4 {target} count 5 source {src_addr_ipv4}"
traceroute = "traceroute ipv4 {target} timeout 1 probe 2 source {src_addr_ipv4}"
[cisco_xr.ipv6]
bgp_route = 'show bgp ipv6 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
ping = "ping ipv6 {target} count 5 source {src_addr_ipv6}"
traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
[[juniper]]
[juniper.dual]
bgp_community = "show route protocol bgp community {target}"
bgp_aspath = "show route protocol bgp aspath-regex {target}"
[juniper.ipv4]
bgp_route = "show route protocol bgp table inet.0 {target} detail"
ping = "ping inet {target} count 5 source {src_addr_ipv4}"
traceroute = "traceroute inet {target} wait 1 source {src_addr_ipv4}"
[juniper.ipv6]
bgp_route = "show route protocol bgp table inet6.0 {target} detail"
ping = "ping inet6 {target} count 5 source {src_addr_ipv6}"
traceroute = "traceroute inet6 {target} wait 1 source {src_addr_ipv6}"
```
Every attempt has been made to filter out as much "noise" as possible from the command output.
##### `{target}`
Maps to search box input.
##### `{src_addr_ipv4}`
Maps to [src_addr_ipv4](configuration/devices.md/#src_addr_ipv4)
##### `{src_addr_ipv6}`
Maps to [src_addr_ipv6](configuration/devices.md/#src_addr_ipv6)

View file

@ -0,0 +1,160 @@
Devices/routers are defined in `hyperglass/hyperglass/config/devices.toml`. `devices.toml` is effectively an array of hash tables/dictionaries/key value pairs:
```toml
[[router]]
address = "10.0.0.1"
asn = "65000"
src_addr_ipv4 = "192.0.2.1"
src_addr_ipv6 = "2001:db8::1"
credential = "default"
location = "pop1"
name = "router1.pop1"
port = "22"
type = "cisco_xr"
proxy = "jumpbox1"
[[router]]
address = "10.0.0.2"
asn = "65000"
src_addr_ipv4 = "192.0.2.2"
src_addr_ipv6 = "2001:db8::2"
credential = "default"
location = "pop2"
name = "router1.pop2"
port = "22"
type = "cisco_ios"
proxy = "jumpbox2"
[[router]]
address = "10.0.0.3"
asn = "65000"
src_addr_ipv4 = "192.0.2.3"
src_addr_ipv6 = "2001:db8::3"
credential = "default"
location = "pop3"
name = "router1.pop3"
port = "22"
type = "juniper"
proxy = "jumpbox3"
```
### Device Keys
#### address
IP address hyperglass will use to connect to the device.
#### asn
ASN this device is a member of.
#### src_addr_ipv4
Source IPv4 address used for `ping` and `traceroute` queries.
#### src_addr_ipv6
Source IPv6 address used for `ping` and `traceroute` queries.
#### credential
Name of credential (username & password) used to authenticate with the device. Credentials are defined as individual tables. See [here](/configuration/authentication.md) for more information on authentication.
#### location
Name of location/POP where this device resides.
#### name
Display name/hostname of device.
#### port
TCP port for SSH connection to device.
#### type
Device type/vendor name as recognized by [Netmiko](https://github.com/ktbyers/netmiko). See [supported device types](#supported-device-types) for a full list.
#### proxy
Name of SSH proxy/jumpbox, if any, used for connecting to the device. See [here](/configuration/proxy.md) for more information on proxying.
### Supported Device Types
Updated **2019-04-28** from [Netmiko](https://github.com/ktbyers/netmiko/blob/master/netmiko/ssh_dispatcher.py#L76).
```console
a10
accedian
alcatel_aos
alcatel_sros
apresia_aeos
arista_eos
aruba_os
avaya_ers
avaya_vsp
brocade_fastiron
brocade_netiron
brocade_nos
brocade_vdx
brocade_vyos
checkpoint_gaia
calix_b6
ciena_saos
cisco_asa
cisco_ios
cisco_nxos
cisco_s300
cisco_tp
cisco_wlc
cisco_xe
cisco_xr
coriant
dell_dnos9
dell_force10
dell_os6
dell_os9
dell_os10
dell_powerconnect
dell_isilon
eltex
enterasys
extreme
extreme_ers
extreme_exos
extreme_netiron
extreme_nos
extreme_slx
extreme_vdx
extreme_vsp
extreme_wing
f5_ltm
f5_tmsh
f5_linux
fortinet
generic_termserver
hp_comware
hp_procurve
huawei
huawei_vrpv8
ipinfusion_ocnos
juniper
juniper_junos
linux
mellanox
mrv_optiswitch
netapp_cdot
netscaler
oneaccess_oneos
ovs_linux
paloalto_panos
pluribus
quanta_mesh
rad_etx
ruckus_fastiron
ubiquiti_edge
ubiquiti_edgeswitch
vyatta_vyos
vyos
```

View file

@ -0,0 +1,132 @@
From `hyperglass/hyperglass/config/config.toml`:
### primary_asn
| Type | Default Value |
| ------ | ------------- |
| String | `"65000"` |
Your network's _primary_ ASN. Number only, e.g. `65000`, **not** `AS65000`.
### debug
| Type | Default Value |
| ------- | ------------- |
| Boolean | `False` |
Enables Flask debugging. May be used to enable other module debugs in the future.
### google_analytics
| Type | Default Value |
| ------ | ------------- |
| String | None |
Google Analytics ID number. For more information on how to set up Google Analytics, see [here](https://support.google.com/analytics/answer/1008080?hl=en).
### message_error
| Type | Default Value |
| ------ | --------------------- |
| String | `"{input} is invalid."` |
Message presented to the user when invalid input is detected. `{input}` will be formatted as the input received from the main search field. For each command, input is validated via regular expression in the following patterns:
| Command | Pattern |
| ------------- | -------------------------------------------- |
| BGP Route | Valid IPv4 or IPv6 Address |
| BGP Community | Valid new-format, 32 bit, or large community |
| BGP AS Path | Any pattern |
| Ping | Valid IPv4 or IPv6 Address |
| Traceroute | Valid IPv4 or IPv6 Address |
!!! note
The BGP AS Path command currently allows `(.*)` to be submitted to the end device. Obviously, the device itself will return an error for garbage input, but ideally this would be "locked down" further. If you have an idea for a regex pattern to validate an `AS_PATH` regex, please submit a PR.
### message_blacklist
| Type | Default Value |
| ------ | ------------------------- |
| String | `"{input} is not allowed."` |
Message presented to the user when an IPv4 or IPv6 address matches the `blacklist.toml` array. `{input}` will be formatted as the input received from the main search field. For information on how this works, please see the [blacklist documentation](/configuration/blacklist).
### message_rate_limit_query
| Type | Default Value |
| ------ | -------------------------------------------------------------------------------------------- |
| String | `"Query limit of {rate_limit_query} per minute reached. Please wait one minute and try again."` |
Message presented to the user when the [query limit](#rate_limit_query) is reached. `{rate_limit_query}` will be formatted as the [`rate_limit_query`](#rate_limit_query) parameter. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query).
### enable_bgp_route
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the BGP Route query type.
### enable_bgp_community
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the BGP Community query type.
### enable_bgp_aspath
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the BGP AS Path query type.
### enable_ping
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the Ping query type.
### enable_traceroute
| Type | Default Value |
| ------- | ------------- |
| Boolean | `True` |
Enables or disables the Traceroute query type.
### rate_limit_query
| Type | Default Value |
| ------- | ------------- |
| String | `"5"` |
Sets the number of queries **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/query).
### rate_limit_site
| Type | Default Value |
| ------- | ------------- |
| String | `"120"` |
Sets the number of site loads **per minute** allowed by `remote_address` of the request. For information on how this works, please see the [rate limiting documentation](/ratelimiting/site).
### cache_timeout
| Type | Default Value |
| -------- | ------------- |
| Integer | `120` |
Sets the number of **seconds** to cache the back-end response. For information on how this works, please see the [caching documentation](/caching).
### cache_directory
| Type | Default Value |
| -------- | ------------------------------------ |
| String | `"hyperglass/hyperglass/.flask_cache"` |
Sets the directory where the back-end responses are cached. For information on how this works, please see the [caching documentation](/caching).

View file

@ -0,0 +1,45 @@
Proxy servers are defined in `hyperglass/hyperglass/config/devices.toml`. Each proxy definition is a unique TOML table, for example:
```toml
[proxy.'jumpbox1']
address = "10.1.1.1"
username = "hyperglass"
password = "secret_password"
type = "linux_ssh"
ssh_command = "ssh -l {username} {host}"
[proxy.'jumpbox2']
address = "10.1.1.2"
username = "hyperglass"
password = "secret_password"
type = "linux_ssh"
ssh_command = "ssh -l {username} {host}"
```
When a proxy server is defined under the `[[router]]` heading in `devices.toml`, the defined proxy name is matched to a configured proxy as shown above. When the connection to the device is initiated, the hyperglass server will first initiate an SSH connection to the proxy, and then initiate a second connection to the target device (router) *from* the proxy server. This can be helpful if you want to secure access to your routers.
#### address
IP address hyperglass will use to connect to the device.
#### username
Username for SSH authentication to the proxy server/jumpbox. SSH Key authentication is not yet supported.
#### password
Plain text password for SSH authentication to the proxy server/jumpbox.
!!! warning "Security Warning"
These values are stored in plain text. Make sure the accounts are restricted and that the configuration file is stored in a secure location.
#### type
Device type/vendor name as recognized by [Netmiko](https://github.com/ktbyers/netmiko). See [supported device types](#supported-device-types) for a full list.
!!! info "Compatibility"
Hyperglass has only been tested with `linux_ssh` as of this writing.
#### ssh_command
Command used to initiate an SSH connection *from* the proxy server to the target device. `{username}` will map to the target device (router) username as defined in its associated credential mapping. `{host}` will map to the target device IP address as defined in `devices.toml`.

127
docs/development/index.md Normal file
View file

@ -0,0 +1,127 @@
# Hyperglass Development
Hyperglass is maintained as a [Github project](https://github.com/checktheroads/hyperglass) under the BSD 3-Clause Clear License. Hyperglass users are encouraged to submit Github issues for feature requests and bug reports.
## License
The intent behind the the [BSD 3-Clause Clear License](https://choosealicense.com/licenses/bsd-3-clause-clear/) is to ensure that anyone can use or modify Hyperglass in any way they wish, as long as credit and copyright notice is provied. If you have any questions about this, or wish to request any special permissions, please contact [matt@allroads.io](mailto:matt@allroads.io).
## Structure
Under the main `hyperglass/hyperglass/` directory, the following basic structure is in place:
```
hyperglass/
├── __init__.py
├── app.py
├── cmd_construct.py
├── cmd_execute.py
├── cmd_parser.py
├── config/
├── manage.py
├── static/
├── templates/
├── templates.py
└── vars.py
```
### Directories
#### config
The `config/` directory contains all TOML config files used by Hyperglass:
```
hyperglass/config/
├── blacklist.toml
├── commands.toml
├── config.toml
├── devices.toml
└── requires_ipv6_cidr.toml
```
#### static
The `static/` directory contains all static HTML/CSS/JS files used for serving the site:
```
hyperglass/static/
├── css
│   ├── hyperglass.css
│   └── icofont
├── images
│   ├── brand.svg
│   ├── favicon
│   ├── hyperglass-dark.png
│   └── hyperglass-light.png
├── js
│   ├── hyperglass.js
│   ├── jquery-3.4.0.min.js
│   └── jquery-3.4.0.min.map
└── sass
├── base
├── components
├── custom
├── elements
├── grid
├── hyperglass.scss
├── layout
└── utilities
```
- `css/hyperglass.css` Final CSS file compiled from Sass file `hyperglass.scss`. Sass compiles all the `.sass` files located under `sass/` and combines them into a single CSS file.
- `css/icofont/` Completely free alternative to FontAwesome - [Icofont](https://icofont.com/).
- `js/hyerpglass.js` Basic Javascript helper to perform AJAX queries necessary to pull in dynamic information and render content.
#### templates
The `templates/` directory contains HTML and Sass Jinja2 templates:
```
templates/
├── 415.html
├── 429.html
├── base.html
├── footer.html
├── footer.md
├── hyperglass.scss
└── index.html
```
- `415.html` General error page template.
- `429.html` Site load rate limit page.
- `base.html` Base template inherited by all other templates. Contains HTML `head`, JavaScript, etc.
- `footer.html` Footer template containing footer text and hyperglass credit link.
- `footer.md` Text that appears in the footer, if enabled. Markdown will be rendered as HTML.
- `hyperglass.scss` Generates SCSS file for Bulma and local customizations.
- `index.html` Main page template.
### Scripts
#### `app.py`
Main Flask application. Passes input to `cmd_execute.py`
#### `cmd_execute.py`
Matches router name to router IP, OS, and credentials. Passes data to `cmd_construct.py`, uses the results to execute the Netmiko action. Also performs error handling in the event of a [blacklist](/configuration/blacklist) match.
#### `cmd_construct.py`
Constructs full commands to run on routers from `hyperglass/hyperglass/config/commands.toml`. Also performs error handling in the event of input errors.
#### `cmd_parser.py`
Parses output before presentation to the user. For the time being, only BGP output from Cisco IOS is parsed. This is because for BGP Community and AS_PATH lookups, Cisco IOS returns results for *all* address families, including VPNv4. This script ensures that only IPv4 and IPv6 address family output is returned.
#### `manage.py`
Management script for perfoming one-off actions. For now, the only action implemented is a manual clearing of the Flask-cache cache.
#### `templates.py`
Renders HTML and Sass templates, compiles Sass to CSS.
#### `vars.py`
Imports configuration from TOML configuration files, defines default values, and exports each as a variable that can be called in other scripts.

View file

@ -0,0 +1,17 @@
| Expression | Match |
| :----------------------- | ----------------------------------------------------: |
| `.\*` | Anything |
| `.+` | One Character |
| `^$` | Local Routes |
| `\_65000$` | Originated by `AS65000` |
| `^65000\_` | Received from `AS65000` |
| `_65000_` | Via `AS65000` |
| `_65000_65001_` | Via `AS65000` and `AS65001` |
| `_(65000_)+` | Multiple `AS65000` in path |
| `^[0-9]+$` | AS_PATH length of 1 |
| `^[0-9]+_[0-9]+$` | AS_PATH length of 2 |
| `^[0-9]*_[0-9]+$` | AS_PATH length of 1 or 2 |
| `^[0-9]*_[0-9]*$` | AS_PATH length of 0, 1, or 2 |
| `^[0-9]+_[0-9]+_[0-9]+$` | AS_PATH length of 3 |
| `_(65000\|65001)_` | Anything that has gone through `AS65000` or `AS65001` |
| `_65000(_.+_)65001$` | Anything from `AS65001` that passed through `AS65001` |

BIN
docs/hyperglass-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

25
docs/index.md Normal file
View file

@ -0,0 +1,25 @@
<img src="/hyperglass-dark.png" width=300></img>
# What is Hyperglass?
**Hyperglass** is an open source looking glass application to provide customers, peers, and partners of network operators with unattended visibility into the operator's network.
# Yet Another Looking Glass?
Many of the more popular open source looking glass applications are written in PHP or Perl, languages infrequently used by many network engineers today. With the widespread adoption of network operations tooling such as [Netmiko](https://github.com/ktbyers/netmiko), [Netbox](https://github.com/digitalocean/netbox), and [Napalm](https://github.com/napalm-automation/napalm), Python is most often the language of choice for network operators. Hyperglass is built completely on Python 3 and utilizes user-friendly configuration files to provide a highly customizable, easy to deploy looking glass app.
Hyperglass was created with the lofty goal of benefiting the internet community at-large, by providing an easier and more familiar way for operators to provide looking glass services to their customers, peers, and partners.
# Application Stack
| Function | Component |
| -------------------------- | --------- |
| Front End Framework | Bulma |
| Front End Application | Flask |
| Back End Application | Python 3 |
| Device Connection Handling | Netmiko |
| Configuration Format | TOML |
# Get Started
See the [installation guide](<>) to get started.

3
docs/ratelimiting.md Normal file
View file

@ -0,0 +1,3 @@
Hyperglass supports configurable rate limiting of both site loads and queries. By default, users are able to reach the site up to 120 times per minute, and submit up to 5 queries per minute. When the site limit is reached, users are directed to a separate error page until the timer expires. When the query limit is reached, an error message is displayed, and no further queries are allowed until the timer expires.
Hyperglass uses [Flask-Limiter](https://github.com/alisaifee/flask-limiter) to handle application rate limiting. In Flask, the `/lg` route, which is used for actual queries, is associated with the query rate limit, while the default `/` route is associated with the site rate limit. Both of these limits are configurabale in `hyperglass/hyperglass/configs/config.toml`. See [here](/configuration/general/#rate_limit_query) for more information.

2
docs/requirements.txt Normal file
View file

@ -0,0 +1,2 @@
pygments
pymdown-extensions

6
hyperglass/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.DS_Store
.sass-cache/
.flask_cache/
test.py
__pycache__/
parsing/

0
hyperglass/__init__.py Normal file
View file

177
hyperglass/app.py Normal file
View file

@ -0,0 +1,177 @@
# Module Imports
import logging
from flask import Flask, request, Response, jsonify, flash
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_caching import Cache
import json
import toml
# Local Imports
import vars
from cmd_execute import cmd_execute
import templates
log = logging.getLogger(__name__)
# Load TOML config file
devices = toml.load(open("./config/devices.toml"))
# Filter config file to list of routers & subsequent configurations
routers_list = devices["router"]
# Filter config file to array of operating systems that require IPv6 BGP lookups in CIDR format
ipv6_cidr_list = toml.load(open("./config/requires_ipv6_cidr.toml"))[
"requires_ipv6_cidr"
]
# Main Flask definition
app = Flask(__name__, static_url_path="/static")
# Flask-Limiter Config
rate_limit_query = vars.gen.rate_limit_query() + " per minute"
rate_limit_site = vars.gen.rate_limit_site() + "per minute"
limiter = Limiter(app, key_func=get_remote_address, default_limits=[rate_limit_site])
# Render Main Flask-Limiter Error Message
@app.errorhandler(429)
def error429(e):
"""Renders full error page for too many site queries"""
html = templates.html.renderTemplate("429")
return html, 429
def error415():
"""Renders full error page for generic errors"""
html = templates.html.renderTemplate("415")
return html, 415
def errorQuery():
"""Renders modal error message"""
return 429
def errorGeneral(id):
"""Renders notification error message with an ID number"""
return "An unknown error occurred." + "\s" + id, 415
# Flask-Caching Config
cache = Cache(
app,
config={
"CACHE_TYPE": "filesystem",
"CACHE_DIR": vars.gen.cache_directory(),
"CACHE_DEFAULT_TIMEOUT": vars.gen.cache_timeout(),
},
)
def clearCache():
"""Function to clear the Flask-Caching cache"""
with app.app_context():
cache.clear()
# Main / Flask route where html is rendered via Jinja2
@app.route("/", methods=["GET"])
@limiter.limit(rate_limit_site)
def site():
"""Main front-end web application"""
html = templates.html.renderTemplate("index")
return html
# Test route for various tests
@app.route("/test", methods=["GET"])
def testRoute():
html = templates.html.renderTemplate("test")
return html
# Flask GET route to provides a JSON list of all networks/ASNs from the config file
@app.route("/networks", methods=["GET"])
def get_networks():
results = []
results_dedup = set(results)
for r in routers_list:
if not r["asn"] in results_dedup:
results_dedup.add(r["asn"])
results.append(dict(network=r["asn"]))
results_json = json.dumps(results, sort_keys=True)
return results_json
# Flask GET route provides a JSON list of all routers for the selected network/ASN
@app.route("/routers/<asn>", methods=["GET"])
def get_routers(asn):
results = []
# For any configured router matching the queried ASN, return only the address/hostname, location, and OS type of the matching routers
for r in routers_list:
if r["asn"] == asn:
if r["type"] in ipv6_cidr_list:
results.append(
dict(
location=r["location"],
hostname=r["name"],
type=r["type"],
requiresIP6Cidr=True,
)
)
else:
results.append(
dict(
location=r["location"],
hostname=r["name"],
type=r["type"],
requiresIP6Cidr=False,
)
)
results_json = json.dumps(results)
return results_json
# Flask POST route ingests data from the JS form submit, passes it to the backend looking glass application to perform the filtering/lookups
@app.route("/lg", methods=["POST"])
# Invoke Flask-Limiter with configured rate limit
@limiter.limit(rate_limit_query)
def lg():
"""Main backend application initiator"""
lg_data = request.get_json()
# Stringify the form response containing serialized JSON for the request, use as key for k/v cache store so each command output value is unique
cache_key = str(lg_data)
# Check if cached entry exists
if cache.get(cache_key) is None:
cache_value = cmd_execute(lg_data)
log.debug(cache_value[1:])
value_output = cache_value[0]
value_code = cache_value[1]
value_params = cache_value[2:]
log.debug("No cache match for: ", cache_key, "\nAdding cache entry...")
# If it doesn't, create a cache entry
try:
cache.set(cache_key, value_output)
log.debug("\nAdded cache entry: ", *value_params)
except:
raise RuntimeError("Unable to add output to cache.", 415, *value_params)
# If 200, return output
if value_code == 200:
return cache.get(cache_key)
# If 400 error, return error message and code
elif value_code in [405, 415]:
return Response(cache.get(cache_key), value_code)
# If it does, return the cached entry
else:
log.debug("Cache match for: ", cache_key, "\nReturning cached entry...")
try:
return cache.get(cache_key)
except:
id = 4152
raise RuntimeError(
id + ":\s" + "Unable to return cached output.", 415, *value_params
)
# Upon exception, render generic error
return Response(errorGeneral(id))
if __name__ == "__main__":
templates.css.renderTemplate()
app.run(host="0.0.0.0", debug=vars.gen.debug(), port=5000)

232
hyperglass/cmd_construct.py Normal file
View file

@ -0,0 +1,232 @@
import sys
import logging
import toml
import re
from netaddr import *
# Local imports
import vars
log = logging.getLogger(__name__)
# Load TOML config file
devices = toml.load(open("./config/devices.toml"))
# Load TOML commands file
commands = toml.load(open("./config/commands.toml"))
# Filter config to router list
routers_list = devices["router"]
# Receives JSON from Flask, constucts the command that will be passed to the router
# Also handles input validation & error handling
def cmd_construct(router, cmd, ipprefix):
inputParams = router, cmd, ipprefix
try:
# Loop through routers config file, match input router with configured routers, set variables
for r in routers_list:
try:
if router == r["address"]:
type = r["type"]
src_addr_ipv4 = r["src_addr_ipv4"]
src_addr_ipv6 = r["src_addr_ipv6"]
# Loop through commands config file, set variables for matched commands
for nos in commands:
if type == nos:
nos = commands[type]
nos_commands = nos[0]
# Dual stack commands (agnostic of IPv4/IPv6)
dual_commands = nos_commands["dual"]
# IPv4 Specific Commands
ipv4_commands = nos_commands["ipv4"]
# IPv6 Specific Commands
ipv6_commands = nos_commands["ipv6"]
if cmd == "Query Type":
msg = "You must select a query type."
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
# BGP Community Query
elif cmd in ["bgp_community"]:
# Extended Communities, new-format
if re.match("^([0-9]{0,5})\:([0-9]{1,5})$", ipprefix):
for a, c in dual_commands.items():
if a == cmd:
command = c.format(target=ipprefix)
msg = "{i} matched new-format community.".format(
i=ipprefix
)
code = 200
log.info(msg, code, router, type, command)
return (msg, code, router, type, command)
# Extended Communities, 32 bit format
elif re.match("^[0-9]{1,10}$", ipprefix):
for a, c in dual_commands.items():
if a == cmd:
command = c.format(target=ipprefix)
msg = "{i} matched 32 bit community.".format(
i=ipprefix
)
code = 200
log.info(msg, code, router, type, command)
return (msg, code, router, type, command)
# RFC 8092 Large Community Support
elif re.match(
"^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$",
ipprefix,
):
for a, c in dual_commands.items():
if a == cmd:
command = c.format(target=ipprefix)
msg = "{i} matched large community.".format(
i=ipprefix
)
code = 200
log.info(msg, code, router, type, command)
return (msg, code, router, type, command)
else:
msg = "{i} is an invalid BGP Community Format.".format(
i=ipprefix
)
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
# BGP AS_PATH Query
elif cmd in ["bgp_aspath"]:
if re.match(".*", ipprefix):
for a, c in dual_commands.items():
if a == cmd:
command = c.format(target=ipprefix)
msg = "{i} matched AS_PATH regex.".format(
i=ipprefix
)
code = 200
log.info(msg, code, router, type, command)
return (msg, code, router, type, command)
else:
msg = "{i} is an invalid AS_PATH regex.".format(
i=ipprefix
)
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
# BGP Route Query
elif cmd in ["bgp_route"]:
try:
# Use netaddr library to verify if input is a valid IPv4 address or prefix
if IPNetwork(ipprefix).ip.version == 4:
for a, c in ipv4_commands.items():
if a == cmd:
command = c.format(target=ipprefix)
msg = "{i} is a valid IPv4 Adddress.".format(
i=ipprefix
)
code = 200
log.info(
msg, code, router, type, command
)
return (
msg,
code,
router,
type,
command,
)
# Use netaddr library to verify if input is a valid IPv6 address or prefix
elif IPNetwork(ipprefix).ip.version == 6:
for a, c in ipv6_commands.items():
if a == cmd:
command = c.format(
target=ipprefix
)
msg = "{i} is a valid IPv6 Adddress.".format(
i=ipprefix
)
code = 200
log.info(
msg,
code,
router,
type,
command,
)
return (
msg,
code,
router,
type,
command,
)
# Exception from netaddr library will return a user-facing error
except:
msg = "{i} is an invalid IP Address.".format(
i=ipprefix
)
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
# Ping/Traceroute
elif cmd in ["ping", "traceroute"]:
try:
if IPNetwork(ipprefix).ip.version == 4:
for a, c in ipv4_commands.items():
if a == cmd:
command = c.format(
target=ipprefix,
src_addr_ipv4=src_addr_ipv4,
)
msg = "{i} is a valid IPv4 Adddress.".format(
i=ipprefix
)
code = 200
log.info(
msg, code, router, type, command
)
return (
msg,
code,
router,
type,
command,
)
elif IPNetwork(ipprefix).ip.version == 6:
for a, c in ipv6_commands.items():
if a == cmd:
command = c.format(
target=ipprefix,
src_addr_ipv6=src_addr_ipv6,
)
msg = "{i} is a valid IPv6 Adddress.".format(
i=ipprefix
)
code = 200
log.info(
msg, code, router, type, command
)
return (
msg,
code,
router,
type,
command,
)
except:
msg = "{i} is an invalid IP Address.".format(
i=ipprefix
)
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
else:
msg = "Command {i} not found.".format(i=cmd)
code = 415
log.error(msg, code, *inputParams)
return (msg, code, *inputParams)
except:
error_msg = log.error(
"Input router IP {router} does not match the configured router IP of {ip}".format(
router=router, ip=r["address"]
)
)
raise ValueError(error_msg)
except:
raise

173
hyperglass/cmd_execute.py Normal file
View file

@ -0,0 +1,173 @@
import sys
import logging
import toml
import time
from netmiko import ConnectHandler
from netmiko import redispatch
from netaddr import *
from cmd_construct import cmd_construct
import vars
import cmd_parser as parser
log = logging.getLogger(__name__)
# Load TOML config file
devices = toml.load(open("./config/devices.toml"))
# Filter config to router list
routers_list = devices["router"]
# Filter config to credential list
credentials_list = devices["credential"]
# Filter config to proxy servers
proxies_list = devices["proxy"]
blacklist_config = toml.load(open("./config/blacklist.toml"))
blacklist = IPSet(blacklist_config["blacklist"])
general_error = "Error connecting to device."
def cmd_execute(lg_data):
# Check POST data from JS, if location matches a configured router's
# location, use the router's configured IP address to connect
for r in routers_list:
if r["location"] == lg_data["router"]:
lg_router_address = r["address"]
# Check blacklist.toml array for prefixes/IPs and return an error upon a match
if lg_data["cmd"] in ["bgp_route", "ping", "traceroute"]:
try:
if IPNetwork(lg_data["ipprefix"]).ip in blacklist:
msg = "{i} is not allowed.".format(i=lg_data["ipprefix"])
code = 405
log.error(msg, code, lg_data)
return (msg, code, lg_data)
# If netaddr library throws an exception, return a user-facing error.
except:
msg = "{i} is not a valid IP Address.".format(i=lg_data["ipprefix"])
code = 415
log.error(msg, code, lg_data)
return (msg, code, lg_data)
# Send "clean" request to cmd_construct to build the command that will be sent to the router
msg, status, router, type, command = cmd_construct(
lg_router_address, lg_data["cmd"], lg_data["ipprefix"]
)
# Loop through proxy config, match configured proxy name for each router with a configured proxy
# Return configured proxy parameters for netmiko
def matchProxy(search_proxy):
if configured_proxy in proxies_list:
proxy_address = proxies_list[search_proxy]["address"]
proxy_username = proxies_list[search_proxy]["username"]
proxy_password = proxies_list[search_proxy]["password"]
proxy_type = proxies_list[search_proxy]["type"]
proxy_ssh_command = proxies_list[search_proxy]["ssh_command"]
return (
proxy_address,
proxy_username,
proxy_password,
proxy_type,
proxy_ssh_command,
)
else:
msg = "Router does not have a proxy configured."
code = 415
log.error(msg, code, lg_data)
return (msg, code, lg_data)
# Matches router with configured credential
def findCred(router):
for r in routers_list:
if r["address"] == router:
configured_credential = r["credential"]
return configured_credential
# Matches configured credential with real username/password
def returnCred(configured_credential):
if configured_credential in credentials_list:
matched_username = credentials_list[configured_credential]["username"]
matched_password = credentials_list[configured_credential]["password"]
return matched_username, matched_password
else:
msg = "Credential {i} does not exist".format(i=configured_credential)
code = 415
log.error(msg, code, lg_data)
return (general_error, code, lg_data)
# Connect to the router via netmiko library, return the command output
def getOutputDirect():
try:
nm_connect_direct = ConnectHandler(**nm_host)
nm_output_direct = nm_connect_direct.send_command(command)
return nm_output_direct
except:
msg = "Unable to reach target {l}".format(l=lg_data["router"])
code = 415
log.error(msg, code, lg_data)
return (general_error, code, lg_data)
# Connect to the proxy server via netmiko library, then log into the router
# via standard SSH
def getOutputProxy(router_proxy):
nm_proxy = {
"host": matchProxy(router_proxy)[0],
"username": matchProxy(router_proxy)[1],
"password": matchProxy(router_proxy)[2],
"device_type": matchProxy(router_proxy)[3],
}
nm_connect_proxied = ConnectHandler(**nm_proxy)
nm_ssh_command = matchProxy(router_proxy)[4].format(**nm_host) + "\n"
nm_connect_proxied.write_channel(nm_ssh_command)
time.sleep(1)
proxy_output = nm_connect_proxied.read_channel()
try:
# Accept SSH key warnings
if "Are you sure you want to continue connecting" in proxy_output:
nm_connect_proxied.write_channel("yes" + "\n")
time.sleep(1)
nm_connect_proxied.write_channel(nm_host["password"] + "\n")
# Send password on prompt
elif "assword" in proxy_output:
nm_connect_proxied.write_channel(nm_host["password"] + "\n")
time.sleep(1)
proxy_output += nm_connect_proxied.read_channel()
# Reclassify netmiko connection as configured device type
redispatch(nm_connect_proxied, nm_host["device_type"])
host_output = nm_connect_proxied.send_command(command)
if host_output:
return host_output
except:
msg = "Proxy server {p} unable to reach target {d}".format(
p=nm_proxy["host"], d=nm_host["host"]
)
code = 415
log.error(msg, code, lg_data)
return (general_error, code, lg_data)
nm_host = {
"host": router,
"device_type": type,
"username": returnCred(findCred(router))[0],
"password": returnCred(findCred(router))[1],
}
# Loop through router list, determine if proxy exists
for r in routers_list:
if r["address"] == router:
configured_proxy = r["proxy"]
if len(configured_proxy) == 0:
connection_proxied = False
else:
connection_proxied = True
if status == 200:
try:
if connection_proxied is True:
output_proxied = getOutputProxy(configured_proxy)
parsed_output = parser.parse(output_proxied, type, lg_data["cmd"])
return parsed_output, status, router, type, command
elif connection_proxied is False:
output_direct = getOutputDirect()
parsed_output = parser.parse(output_direct, type, lg_data["cmd"])
return parsed_output, status, router, type, command
except:
raise
else:
return msg, status, router, type, command

17
hyperglass/cmd_parser.py Normal file
View file

@ -0,0 +1,17 @@
def parse(output, type, cmd):
"""Splits Cisco IOS BGP output by AFI, returns only IPv4 & IPv6 output for protocol-agnostic commands (Community & AS_PATH Lookups)"""
try:
if cmd in ["bgp_community", "bgp_aspath"] and type in ["cisco_ios"]:
delimiter = "For address family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
return delimiter + parsed_ipv4 + delimiter + parsed_ipv6
else:
return output
if cmd in ["bgp_community", "bgp_aspath"] and type in ["cisco_xr"]:
delimiter = "Address Family: "
parsed_ipv4 = output.split(delimiter)[1]
parsed_ipv6 = output.split(delimiter)[2]
return delimiter + parsed_ipv4 + delimiter + parsed_ipv6
except:
raise

2
hyperglass/config/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
*.toml

View file

@ -0,0 +1,8 @@
# Define networks that you don't want users to be able to query. Any IP inside the subnet will return an error message.
blacklist = [
'100.64.0.0/12',
'198.18.0.0/15',
'10.0.0.0/8',
'192.168.0.0/16',
'172.16.0.0/12'
]

View file

@ -0,0 +1,41 @@
# Cisco IOS/IOS-XE
[[cisco_ios]]
[cisco_ios.dual]
bgp_community = "show bgp all community {target}"
bgp_aspath = 'show bgp all quote-regexp "{target}"'
[cisco_ios.ipv4]
bgp_route = "show bgp ipv4 unicast {target} | exclude pathid:|Epoch"
ping = "ping {target} repeat 5 source {src_addr_ipv4}"
traceroute = "traceroute {target} timeout 1 probe 2 source {src_addr_ipv4}"
[cisco_ios.ipv6]
bgp_route = "show bgp ipv6 unicast {target} | exclude pathid:|Epoch"
ping = "ping ipv6 {target} repeat 5 source {src_addr_ipv6}"
traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
# Cisco IOS-XR
[[cisco_xr]]
[cisco_xr.dual]
bgp_community = 'show bgp all unicast community {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
bgp_aspath = 'show bgp all unicast regexp {target} | utility egrep -v "\(BGP |Table |Non-stop\)"'
[cisco_xr.ipv4]
bgp_route = 'show bgp ipv4 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
ping = "ping ipv4 {target} count 5 source {src_addr_ipv4}"
traceroute = "traceroute ipv4 {target} timeout 1 probe 2 source {src_addr_ipv4}"
[cisco_xr.ipv6]
bgp_route = 'show bgp ipv6 unicast {target} | util egrep "\(BGP routing table entry|Path \#|aggregated by|Origin |Community:|validity| from \)"'
ping = "ping ipv6 {target} count 5 source {src_addr_ipv6}"
traceroute = "traceroute ipv6 {target} timeout 1 probe 2 source {src_addr_ipv6}"
# Juniper
[[juniper]]
[juniper.dual]
bgp_community = "show route protocol bgp community {target}"
bgp_aspath = "show route protocol bgp aspath-regex {target}"
[juniper.ipv4]
bgp_route = "show route protocol bgp table inet.0 {target} detail"
ping = "ping inet {target} count 5 source {src_addr_ipv4}"
traceroute = "traceroute inet {target} wait 1 source {src_addr_ipv4}"
[juniper.ipv6]
bgp_route = "show route protocol bgp table inet6.0 {target} detail"
ping = "ping inet6 {target} count 5 source {src_addr_ipv6}"
traceroute = "traceroute inet6 {target} wait 1 source {src_addr_ipv6}"

View file

@ -0,0 +1,55 @@
# General site-wide parameters
[[general]]
primary_asn = ""
debug = true
google_analytics = ""
message_error = ""
message_blacklist = ""
message_rate_limit_query = ""
enable_bgp_route = true
enable_bgp_community = true
enable_bgp_aspath = true
enable_ping = true
enable_traceroute = true
rate_limit_query = ""
rate_limit_site = ""
cache_timeout = 120
cache_directory = ""
# Branding/Site Customization Parameters
[[branding]]
site_title = ""
title_mode = ""
title = ""
subtitle = ""
enable_footer = true
enable_credit = true
color_bg = ""
color_btn_submit = ""
color_tag_loctitle = ""
color_tag_cmdtitle = ""
color_tag_cmd = ""
color_tag_loc = ""
color_hero = ""
color_progressbar = ""
logo_path = ""
logo_width = ""
placeholder_prefix = ""
show_peeringdb = true
text_results = ""
text_location = ""
text_cache = ""
text_limiter_title = ""
text_limiter_subtitle = ""
text_415_title = ""
text_415_subtitle = ""
text_415_button = ""
text_help_bgp_route = ""
text_help_bgp_community = ""
text_help_bgp_aspath = ""
text_help_ping = ""
text_help_traceroute = ""
primary_font_url = ""
primary_font_name = ""
mono_font_url = ""
mono_font_name = ""

View file

@ -0,0 +1,54 @@
# Routers
[[router]]
address = "10.0.0.1"
asn = "65000"
src_addr_ipv4 = "192.0.2.1"
src_addr_ipv6 = "2001:db8::1"
credential = "default"
location = "pop01"
name = "router01"
port = "22"
type = "cisco_xr"
proxy = ""
[[router]]
address = "10.0.0.2"
asn = "65000"
src_addr_ipv4 = "192.0.2.2"
src_addr_ipv6 = "2001:db8::2"
credential = "default"
location = "pop02"
name = "router02"
port = "22"
type = "cisco_ios"
proxy = "server01"
[[router]]
address = "10.0.0.3"
asn = "65000"
src_addr_ipv4 = "192.0.2.3"
src_addr_ipv6 = "2001:db8::3"
credential = "other"
location = "pop03"
name = "router03"
port = "22"
type = "juniper"
proxy = ""
# Router Credentials
[credential.'default']
username = "username"
password = "password"
[credential.'other']
username = "otheruser"
password = "otherpass"
# SSH Proxy Servers
[proxy.'server01']
address = "10.0.1.1"
username = "username"
password = "password"
type = "linux_ssh"
ssh_command = "ssh -l {username} {host}"

View file

@ -0,0 +1,4 @@
requires_ipv6_cidr = [
"cisco_ios",
"cisco_nxos"
]

20
hyperglass/manage.py Normal file
View file

@ -0,0 +1,20 @@
import os
import sys
import app
def clearcache():
try:
app.clearCache()
except:
raise
for arg in sys.argv:
try:
if arg == "clearcache":
clearcache()
print("Successfully cleared cache.")
except:
print("Failed to clear cache.")
raise

4
hyperglass/static/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.DS_Store
node_modules/
package-lock.json

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search" class="svg-inline--fa fa-search fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path></svg>

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="static/img/mstile-150x150.png"/>
<TileColor>#330036</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,64 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1014.000000pt" height="1014.000000pt" viewBox="0 0 1014.000000 1014.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1014.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4809 10126 c-2 -2 -51 -7 -109 -10 -92 -6 -211 -18 -305 -30 -16 -3
-52 -8 -80 -11 -27 -4 -138 -24 -245 -46 -516 -102 -1052 -302 -1491 -556 -48
-27 -93 -53 -100 -56 -30 -14 -217 -138 -333 -220 -432 -307 -811 -676 -1130
-1102 -213 -283 -391 -582 -540 -905 -145 -311 -255 -631 -331 -956 -19 -82
-37 -167 -41 -189 -3 -22 -7 -42 -9 -45 -2 -3 -6 -24 -9 -46 -4 -22 -8 -51
-11 -64 -3 -14 -14 -92 -24 -175 -18 -134 -24 -193 -37 -380 -5 -75 -5 -429 1
-520 11 -199 29 -383 45 -470 5 -27 11 -69 14 -92 16 -128 97 -477 156 -668
133 -435 341 -881 591 -1267 40 -62 76 -115 79 -118 3 -3 20 -27 39 -55 44
-64 226 -300 267 -345 17 -19 67 -75 111 -125 143 -160 349 -359 508 -490 33
-28 62 -53 65 -56 3 -3 59 -46 125 -96 552 -416 1168 -712 1831 -877 99 -25
193 -47 209 -50 17 -3 68 -12 115 -21 47 -8 99 -17 115 -20 17 -2 62 -8 100
-14 39 -5 102 -13 140 -16 39 -4 81 -8 95 -10 152 -23 651 -23 890 -1 83 8
186 19 201 21 9 2 40 6 69 10 29 4 60 8 69 10 9 1 36 6 61 9 25 4 99 18 165
32 66 13 129 26 140 28 211 43 664 194 900 301 125 57 372 182 458 233 49 28
91 52 93 52 10 0 306 199 410 275 457 338 859 756 1177 1221 178 262 372 614
472 859 20 50 43 104 50 120 76 172 208 608 251 830 2 11 6 31 9 45 16 76 42
229 50 299 4 30 8 61 10 70 1 9 6 50 9 91 4 41 9 91 11 110 21 173 21 731 1
860 -3 14 -7 59 -11 100 -4 41 -8 86 -10 100 -2 14 -6 41 -9 60 -2 19 -8 58
-11 85 -6 41 -66 351 -80 405 -39 163 -145 506 -188 610 -6 14 -11 27 -12 30
-26 78 -121 289 -205 455 -197 391 -422 723 -735 1085 -78 91 -413 416 -522
507 -272 227 -507 389 -805 556 -126 70 -359 186 -436 216 -27 11 -69 29 -93
39 -255 114 -781 267 -1059 307 -24 4 -47 8 -50 10 -4 2 -35 6 -71 10 -35 3
-72 8 -81 11 -18 5 -50 9 -168 19 -41 4 -91 9 -110 12 -41 5 -646 14 -651 9z
m527 -1010 c67 -4 133 -9 145 -11 13 -2 45 -6 72 -9 26 -3 68 -9 95 -12 187
-23 542 -109 759 -185 215 -76 581 -243 675 -310 14 -11 28 -19 31 -19 21 0
304 -191 441 -298 108 -83 126 -99 236 -196 185 -166 420 -427 563 -626 9 -14
37 -53 62 -87 180 -251 389 -667 504 -1008 72 -215 146 -522 165 -690 2 -16 7
-50 10 -75 22 -146 31 -304 30 -540 -1 -214 -7 -354 -19 -420 -2 -14 -7 -52
-11 -85 -3 -33 -8 -69 -10 -80 -2 -10 -6 -37 -9 -60 -35 -241 -152 -645 -260
-900 -59 -138 -178 -378 -233 -469 -23 -37 -42 -70 -42 -71 0 -14 -267 -396
-290 -415 -3 -3 -15 -17 -26 -32 -72 -93 -203 -236 -334 -364 -508 -494 -1148
-850 -1839 -1023 -168 -42 -380 -81 -521 -95 -25 -3 -76 -8 -115 -13 -142 -15
-691 -8 -792 11 -10 2 -43 7 -74 10 -31 4 -65 8 -75 10 -11 2 -41 7 -68 10
-27 4 -52 9 -55 10 -3 2 -27 7 -52 10 -81 11 -389 93 -529 141 -413 142 -798
346 -1145 609 -327 248 -663 597 -888 926 -376 548 -621 1187 -692 1805 -3 28
-8 64 -10 80 -25 188 -25 664 0 840 3 17 7 53 10 80 29 264 110 602 211 880
292 808 844 1513 1560 1993 194 130 481 286 654 356 36 15 74 31 85 36 110 52
496 172 662 205 133 27 269 49 374 61 30 3 65 8 79 10 14 2 86 7 160 10 74 4
136 8 138 9 4 3 244 -3 368 -9z"/>
<path d="M3445 8398 c-84 -32 -397 -246 -566 -388 -223 -187 -442 -415 -652
-680 -123 -155 -312 -465 -420 -690 -74 -154 -190 -440 -202 -500 -1 -3 -15
-50 -32 -105 -30 -96 -77 -280 -87 -337 -3 -15 -11 -63 -20 -105 -15 -79 -20
-108 -31 -198 -4 -27 -8 -60 -10 -73 -13 -79 -20 -239 -20 -457 0 -280 5 -309
70 -377 55 -60 102 -79 191 -79 88 -1 173 52 217 135 20 38 22 56 23 261 2
262 11 413 34 555 5 30 12 73 15 94 3 21 7 48 10 60 3 12 7 33 10 49 12 64 17
88 46 197 124 482 347 931 658 1325 151 191 360 403 534 543 40 31 74 59 77
63 12 13 222 159 300 208 103 65 136 94 162 145 60 117 16 265 -97 332 -53 31
-158 42 -210 22z"/>
<path d="M1691 4003 c-91 -33 -161 -138 -161 -241 0 -74 135 -389 275 -641
107 -195 164 -243 285 -244 138 -1 246 98 254 232 4 63 -12 111 -73 216 -68
117 -166 320 -226 467 -50 124 -86 172 -153 204 -50 24 -147 27 -201 7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1,19 @@
{
"name": "hyperglass",
"short_name": "hyperglass",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#330036",
"background_color": "#330036",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View file

@ -0,0 +1,258 @@
// Get the list of routers for the selected Network
var progress = ($('#progress'));
var resultsbox = ($('#resultsbox'));
resultsbox.hide();
progress.hide();
listNetworks ();
clientIP ();
function clientIP () {
$.getJSON("https://jsonip.com?callback=?", function(data) {
clientip = data.ip
});
};
function listNetworks () {
let networklist = $('#network');
networklist.empty();
networklist.prop('selectedIndex', 0);
const url = '/networks';
$.getJSON(url, function (data) {
$.each(data, function (key, entry) {
networklist.append($('<option></option>').attr('value', entry.network).text('AS'+entry.network));
})
})
}
// Update the list of routers for the *default* selected network
$( document ).ready(function(){
var defaultasn = $ ( "#network" ).val();
$.ajax({
url: `/routers/${defaultasn}`,
context: document.body,
type: 'get',
success: function (data) {
selectedRouters = JSON.parse(data)
console.log(selectedRouters)
updateRouters(selectedRouters);
},
error: function (err) {
console.log(err)
}
})
})
$('#network').on('change', () => {
var asn = $("select[id=network").val()
$.ajax({
url: `/routers/${asn}`,
type: 'get',
success: function (data) {
updateRouters(JSON.parse(data));
},
error: function (err) {
console.log(err)
}
})
})
function updateRouters (routers) {
routers.forEach(function (r) {
$('#router').append($("<option>").attr('value', r.location).attr('type', r.type).text(r.location))
})
}
// Submit Form Action
$('#lgForm').on('submit', function () {
// Regex to match any IPv4 host address or CIDR prefix
var ipv4_any = new RegExp('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/(3[0-2]|2[0-9]|1[0-9]|[0-9]))?$');
// Regex to match any IPv6 host address or CIDR prefix
var ipv6_any = new RegExp('^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/((1(1[0-9]|2[0-8]))|([0-9][0-9])|([0-9])))?$');
// Regex to match an IPv4 CIDR prefix only (excludes a host address)
var ipv4_cidr = new RegExp('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|2[0-9]|1[0-9]|[0-9])?$');
// Regex to match an IPv6 CIDR prefix only (excludes a host address)
var ipv6_cidr = new RegExp('^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/((1(1[0-9]|2[0-8]))|([0-9][0-9])|([0-9]))?$');
var ipv6_host = new RegExp('^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))?$')
var cmd = $('#cmd option:selected').val();
var routerType = $('#router option:selected').attr('type');
var ipprefix = $('#ipprefix').val();
var router = $('#router option:selected').val();
// Filters selectedRouters JSON object to only the selected router, returns all attributes passed from Flask's `get_routers`
var routersJson = selectedRouters.filter(r => r.location === router);
// Filters above to value of `requiresIP6Cidr` as passed from Flask's `get_routers`
var requiresIP6Cidr = routersJson[0].requiresIP6Cidr
// If BGP lookup, and lookup is an IPv6 address *without* CIDR prefix (e.g. 2001:db8::1, NOT 2001:db8::/48), and requiresIP6Cidr
// is true, show an error.
$('#ipprefix_error').hide()
$('#ipprefix').removeClass('is-danger')
if (cmd == 'bgp_route' && ipv6_host.test(ipprefix) == true && requiresIP6Cidr == true) {
console.log('matched requires ipv6 cidr')
$('#ipprefix_error').show()
$('#ipprefix').addClass('is-danger')
$('#ipprefix_error').html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">
Invalid Input
</div>
<div id="error" style="display: block;" class="message-body">
This router requires IPv6 BGP lookups to be and exact match in CIDR notation.
</div>
</article>
`);
}
// If ping, and lookup is an IPv4 address *with* CIDR prefix (e.g. 192.0.2.0/24, NOT 192.0.2.1), show an error.
else if (ipv4_cidr.test(ipprefix) == true && cmd == 'ping') {
$('#ipprefix_error').show()
$('#ipprefix').addClass('is-danger')
$('#ipprefix_error').html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">
Invalid Input
</div>
<div id="error" style="display: block;" class="message-body">
<code>ping</code> does not allow network masks.
</div>
</article>
`);
}
// If traceroute, and lookup is an IPv4 address *with* CIDR prefix (e.g. 192.0.2.0/24, NOT 192.0.2.1), show an error.
else if (ipv4_cidr.test(ipprefix) == true && cmd == 'traceroute') {
$('#ipprefix_error').show()
$('#ipprefix').addClass('is-danger')
$('#ipprefix_error').html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">
Invalid Input
</div>
<div id="error" style="display: block;" class="message-body">
<code>traceroute</code> does not allow network masks.
</div>
</article>
`);
}
// If ping, and lookup is an IPv6 address *with* CIDR prefix (e.g. 2001:db8::/48, NOT 2001:db8::1), show an error.
else if (ipv6_cidr.test(ipprefix) == true && cmd == 'ping') {
$('#ipprefix_error').show()
$('#ipprefix').addClass('is-danger')
$('#ipprefix_error').html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">
Invalid Input
</div>
<div id="error" style="display: block;" class="message-body">
<code>ping</code> does not allow network masks.
</div>
</article>
`);
}
// If traceroute, and lookup is an IPv6 address *with* CIDR prefix (e.g. 2001:db8::/48, NOT 2001:db8::1), show an error.
else if (ipv6_cidr.test(ipprefix) == true && cmd == 'traceroute') {
$('#ipprefix_error').show()
$('#ipprefix').addClass('is-danger')
$('#ipprefix_error').html(`
<br>
<article class="message is-danger is-small" style="display: block;">
<div class="message-header" style="display: block;">
Invalid Input
</div>
<div id="error" style="display: block;" class="message-body">
<code>traceroute</code> does not allow network masks.
</div>
</article>
`);
}
else submitForm();
});
var submitForm = function() {
progress.hide();
var cmd = $('#cmd option:selected').val();
var cmdtitle = cmd.replace('_', ': ');
var network = $('#network option:selected').val();
var router = $('#router option:selected').val();
var routername = $('#router option:selected').text();
var ipprefix = $('#ipprefix').val();
var routerType = $('#router option:selected').attr('type');
$('#output').text("")
$('#queryInfo').text("")
$('#queryInfo').html(`
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag lg-tag-loctitle">AS${network}</span>
<span class="tag lg-tag-loc">${routername}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag lg-tag-cmdtitle">${cmdtitle}</span>
<span class="tag lg-tag-cmd">${ipprefix}</span>
</div>
</div>
</div>
`)
var xhr = new XMLHttpRequest();
xhr.open('POST', '/lg', true);
resultsbox.show()
progress.show()
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.send(JSON.stringify({router: router, cmd: cmd, ipprefix: ipprefix}))
console.log(JSON.stringify({router: router, cmd: cmd, ipprefix: ipprefix}));
xhr_timer = window.setInterval(function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
progress.hide();
window.clearTimeout(xhr_timer);
}
var output = document.getElementById('output')
if (xhr.status == 415){
console.log(XMLHttpRequest.status, 'error')
var output = document.getElementById('output')
$('#ipprefix').addClass('is-danger')
output.innerHTML =
'<br>' +
'<div class="notification is-danger" id="output">' +
xhr.responseText +
'</div>'
}
if (xhr.status == 405){
console.log(XMLHttpRequest.status, 'error')
var output = document.getElementById('output')
$('#ipprefix').addClass('is-warning')
output.innerHTML =
'<br>' +
'<div class="notification is-warning" id="output">' +
xhr.responseText +
'</div>'
}
else if (xhr.status == 200){
console.log(xhr.status, 'success')
output.innerHTML =
'<br>' +
'<div class="content">' +
'<p class="query-output" id="output">' +
xhr.responseText +
'</p>' +
'</div>'
}
else if (xhr.status == 429){
console.log(xhr.status, 'rate limit reached');
$("#ratelimit").addClass("is-active");
}
}, 500);
xhr.addEventListener("error", function(e) {
console.log("error: " + e);
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
@charset "utf-8"
@import "minireset.sass"
@import "generic.sass"
@import "helpers.sass"

View file

@ -0,0 +1,130 @@
$body-background-color: $white !default
$body-size: 16px !default
$body-rendering: optimizeLegibility !default
$body-family: $family-primary !default
$body-color: $text !default
$body-weight: $weight-normal !default
$body-line-height: 1.5 !default
$code-family: $family-code !default
$code-padding: 0.25em 0.5em 0.25em !default
$code-weight: normal !default
$code-size: 0.875em !default
$hr-background-color: $background !default
$hr-height: 2px !default
$hr-margin: 1.5rem 0 !default
$strong-color: $text-strong !default
$strong-weight: $weight-bold !default
html
background-color: $body-background-color
font-size: $body-size
-moz-osx-font-smoothing: grayscale
-webkit-font-smoothing: antialiased
min-width: 300px
overflow-x: hidden
overflow-y: scroll
text-rendering: $body-rendering
text-size-adjust: 100%
article,
aside,
figure,
footer,
header,
hgroup,
section
display: block
body,
button,
input,
select,
textarea
font-family: $body-family
code,
pre
-moz-osx-font-smoothing: auto
-webkit-font-smoothing: auto
font-family: $code-family
body
color: $body-color
font-size: 1rem
font-weight: $body-weight
line-height: $body-line-height
// Inline
a
color: $link
cursor: pointer
text-decoration: none
strong
color: currentColor
&:hover
color: $link-hover
code
background-color: $code-background
color: $code
font-size: $code-size
font-weight: $code-weight
padding: $code-padding
hr
background-color: $hr-background-color
border: none
display: block
height: $hr-height
margin: $hr-margin
img
height: auto
max-width: 100%
input[type="checkbox"],
input[type="radio"]
vertical-align: baseline
small
font-size: 0.875em
span
font-style: inherit
font-weight: inherit
strong
color: $strong-color
font-weight: $strong-weight
// Block
fieldset
border: none
pre
+overflow-touch
background-color: $pre-background
color: $pre
font-size: 0.875em
overflow-x: auto
padding: 1.25rem 1.5rem
white-space: pre
word-wrap: normal
code
background-color: transparent
color: currentColor
font-size: 1em
padding: 0
table
td,
th
text-align: left
vertical-align: top
th
color: $text-strong

View file

@ -0,0 +1,276 @@
// Float
.is-clearfix
+clearfix
.is-pulled-left
float: left !important
.is-pulled-right
float: right !important
// Overflow
.is-clipped
overflow: hidden !important
// Overlay
.is-overlay
@extend %overlay
// Typography
=typography-size($target:'')
@each $size in $sizes
$i: index($sizes, $size)
.is-size-#{$i}#{if($target == '', '', '-' + $target)}
font-size: $size !important
+typography-size()
+mobile
+typography-size('mobile')
+tablet
+typography-size('tablet')
+touch
+typography-size('touch')
+desktop
+typography-size('desktop')
+widescreen
+typography-size('widescreen')
+fullhd
+typography-size('fullhd')
$alignments: ('centered': 'center', 'justified': 'justify', 'left': 'left', 'right': 'right')
@each $alignment, $text-align in $alignments
.has-text-#{$alignment}
text-align: #{$text-align} !important
@each $alignment, $text-align in $alignments
+mobile
.has-text-#{$alignment}-mobile
text-align: #{$text-align} !important
+tablet
.has-text-#{$alignment}-tablet
text-align: #{$text-align} !important
+tablet-only
.has-text-#{$alignment}-tablet-only
text-align: #{$text-align} !important
+touch
.has-text-#{$alignment}-touch
text-align: #{$text-align} !important
+desktop
.has-text-#{$alignment}-desktop
text-align: #{$text-align} !important
+desktop-only
.has-text-#{$alignment}-desktop-only
text-align: #{$text-align} !important
+widescreen
.has-text-#{$alignment}-widescreen
text-align: #{$text-align} !important
+widescreen-only
.has-text-#{$alignment}-widescreen-only
text-align: #{$text-align} !important
+fullhd
.has-text-#{$alignment}-fullhd
text-align: #{$text-align} !important
.is-capitalized
text-transform: capitalize !important
.is-lowercase
text-transform: lowercase !important
.is-uppercase
text-transform: uppercase !important
.is-italic
font-style: italic !important
@each $name, $pair in $colors
$color: nth($pair, 1)
.has-text-#{$name}
color: $color !important
a.has-text-#{$name}
&:hover,
&:focus
color: darken($color, 10%) !important
.has-background-#{$name}
background-color: $color !important
@each $name, $shade in $shades
.has-text-#{$name}
color: $shade !important
.has-background-#{$name}
background-color: $shade !important
.has-text-weight-light
font-weight: $weight-light !important
.has-text-weight-normal
font-weight: $weight-normal !important
.has-text-weight-semibold
font-weight: $weight-semibold !important
.has-text-weight-bold
font-weight: $weight-bold !important
.is-family-primary
font-family: $family-primary !important
.is-family-secondary
font-family: $family-secondary !important
.is-family-sans-serif
font-family: $family-sans-serif !important
.is-family-monospace
font-family: $family-monospace !important
.is-family-code
font-family: $family-code !important
// Visibility
$displays: 'block' 'flex' 'inline' 'inline-block' 'inline-flex'
@each $display in $displays
.is-#{$display}
display: #{$display} !important
+mobile
.is-#{$display}-mobile
display: #{$display} !important
+tablet
.is-#{$display}-tablet
display: #{$display} !important
+tablet-only
.is-#{$display}-tablet-only
display: #{$display} !important
+touch
.is-#{$display}-touch
display: #{$display} !important
+desktop
.is-#{$display}-desktop
display: #{$display} !important
+desktop-only
.is-#{$display}-desktop-only
display: #{$display} !important
+widescreen
.is-#{$display}-widescreen
display: #{$display} !important
+widescreen-only
.is-#{$display}-widescreen-only
display: #{$display} !important
+fullhd
.is-#{$display}-fullhd
display: #{$display} !important
.is-hidden
display: none !important
.is-sr-only
border: none !important
clip: rect(0, 0, 0, 0) !important
height: 0.01em !important
overflow: hidden !important
padding: 0 !important
position: absolute !important
white-space: nowrap !important
width: 0.01em !important
+mobile
.is-hidden-mobile
display: none !important
+tablet
.is-hidden-tablet
display: none !important
+tablet-only
.is-hidden-tablet-only
display: none !important
+touch
.is-hidden-touch
display: none !important
+desktop
.is-hidden-desktop
display: none !important
+desktop-only
.is-hidden-desktop-only
display: none !important
+widescreen
.is-hidden-widescreen
display: none !important
+widescreen-only
.is-hidden-widescreen-only
display: none !important
+fullhd
.is-hidden-fullhd
display: none !important
.is-invisible
visibility: hidden !important
+mobile
.is-invisible-mobile
visibility: hidden !important
+tablet
.is-invisible-tablet
visibility: hidden !important
+tablet-only
.is-invisible-tablet-only
visibility: hidden !important
+touch
.is-invisible-touch
visibility: hidden !important
+desktop
.is-invisible-desktop
visibility: hidden !important
+desktop-only
.is-invisible-desktop-only
visibility: hidden !important
+widescreen
.is-invisible-widescreen
visibility: hidden !important
+widescreen-only
.is-invisible-widescreen-only
visibility: hidden !important
+fullhd
.is-invisible-fullhd
visibility: hidden !important
// Other
.is-marginless
margin: 0 !important
.is-paddingless
padding: 0 !important
.is-radiusless
border-radius: 0 !important
.is-shadowless
box-shadow: none !important
.is-unselectable
@extend %unselectable

View file

@ -0,0 +1,84 @@
/*! minireset.css v0.0.4 | MIT License | github.com/jgthms/minireset.css */
// Blocks
html,
body,
p,
ol,
ul,
li,
dl,
dt,
dd,
blockquote,
figure,
fieldset,
legend,
textarea,
pre,
iframe,
hr,
h1,
h2,
h3,
h4,
h5,
h6
margin: 0
padding: 0
// Headings
h1,
h2,
h3,
h4,
h5,
h6
font-size: 100%
font-weight: normal
// List
ul
list-style: none
// Form
button,
input,
select,
textarea
margin: 0
// Box sizing
html
box-sizing: border-box
*
&,
&::before,
&::after
box-sizing: inherit
// Media
img,
embed,
iframe,
object,
video
height: auto
max-width: 100%
audio
max-width: 100%
// Iframe
iframe
border: 0
// Table
table
border-collapse: collapse
border-spacing: 0
td,
th
padding: 0
text-align: left

View file

@ -0,0 +1,15 @@
@charset "utf-8"
@import "breadcrumb.sass"
@import "card.sass"
@import "dropdown.sass"
@import "level.sass"
@import "list.sass"
@import "media.sass"
@import "menu.sass"
@import "message.sass"
@import "modal.sass"
@import "navbar.sass"
@import "pagination.sass"
@import "panel.sass"
@import "tabs.sass"

View file

@ -0,0 +1,75 @@
$breadcrumb-item-color: $link !default
$breadcrumb-item-hover-color: $link-hover !default
$breadcrumb-item-active-color: $text-strong !default
$breadcrumb-item-padding-vertical: 0 !default
$breadcrumb-item-padding-horizontal: 0.75em !default
$breadcrumb-item-separator-color: $grey-light !default
.breadcrumb
@extend %block
@extend %unselectable
font-size: $size-normal
white-space: nowrap
a
align-items: center
color: $breadcrumb-item-color
display: flex
justify-content: center
padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal
&:hover
color: $breadcrumb-item-hover-color
li
align-items: center
display: flex
&:first-child a
padding-left: 0
&.is-active
a
color: $breadcrumb-item-active-color
cursor: default
pointer-events: none
& + li::before
color: $breadcrumb-item-separator-color
content: "\0002f"
ul,
ol
align-items: flex-start
display: flex
flex-wrap: wrap
justify-content: flex-start
.icon
&:first-child
margin-right: 0.5em
&:last-child
margin-left: 0.5em
// Alignment
&.is-centered
ol,
ul
justify-content: center
&.is-right
ol,
ul
justify-content: flex-end
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
// Styles
&.has-arrow-separator
li + li::before
content: "\02192"
&.has-bullet-separator
li + li::before
content: "\02022"
&.has-dot-separator
li + li::before
content: "\000b7"
&.has-succeeds-separator
li + li::before
content: "\0227B"

View file

@ -0,0 +1,74 @@
$card-color: $text !default
$card-background-color: $white !default
$card-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
$card-header-background-color: transparent !default
$card-header-color: $text-strong !default
$card-header-shadow: 0 1px 2px rgba($black, 0.1) !default
$card-header-weight: $weight-bold !default
$card-content-background-color: transparent !default
$card-footer-background-color: transparent !default
$card-footer-border-top: 1px solid $border !default
.card
background-color: $card-background-color
box-shadow: $card-shadow
color: $card-color
max-width: 100%
position: relative
.card-header
background-color: $card-header-background-color
align-items: stretch
box-shadow: $card-header-shadow
display: flex
.card-header-title
align-items: center
color: $card-header-color
display: flex
flex-grow: 1
font-weight: $card-header-weight
padding: 0.75rem
&.is-centered
justify-content: center
.card-header-icon
align-items: center
cursor: pointer
display: flex
justify-content: center
padding: 0.75rem
.card-image
display: block
position: relative
.card-content
background-color: $card-content-background-color
padding: 1.5rem
.card-footer
background-color: $card-footer-background-color
border-top: $card-footer-border-top
align-items: stretch
display: flex
.card-footer-item
align-items: center
display: flex
flex-basis: 0
flex-grow: 1
flex-shrink: 0
justify-content: center
padding: 0.75rem
&:not(:last-child)
border-right: $card-footer-border-top
// Combinations
.card
.media:not(:last-child)
margin-bottom: 0.75rem

View file

@ -0,0 +1,77 @@
$dropdown-content-background-color: $white !default
$dropdown-content-arrow: $link !default
$dropdown-content-offset: 4px !default
$dropdown-content-radius: $radius !default
$dropdown-content-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
$dropdown-content-z: 20 !default
$dropdown-item-color: $grey-dark !default
$dropdown-item-hover-color: $black !default
$dropdown-item-hover-background-color: $background !default
$dropdown-item-active-color: $link-invert !default
$dropdown-item-active-background-color: $link !default
$dropdown-divider-background-color: $border !default
.dropdown
display: inline-flex
position: relative
vertical-align: top
&.is-active,
&.is-hoverable:hover
.dropdown-menu
display: block
&.is-right
.dropdown-menu
left: auto
right: 0
&.is-up
.dropdown-menu
bottom: 100%
padding-bottom: $dropdown-content-offset
padding-top: initial
top: auto
.dropdown-menu
display: none
left: 0
min-width: 12rem
padding-top: $dropdown-content-offset
position: absolute
top: 100%
z-index: $dropdown-content-z
.dropdown-content
background-color: $dropdown-content-background-color
border-radius: $dropdown-content-radius
box-shadow: $dropdown-content-shadow
padding-bottom: 0.5rem
padding-top: 0.5rem
.dropdown-item
color: $dropdown-item-color
display: block
font-size: 0.875rem
line-height: 1.5
padding: 0.375rem 1rem
position: relative
a.dropdown-item,
button.dropdown-item
padding-right: 3rem
text-align: left
white-space: nowrap
width: 100%
&:hover
background-color: $dropdown-item-hover-background-color
color: $dropdown-item-hover-color
&.is-active
background-color: $dropdown-item-active-background-color
color: $dropdown-item-active-color
.dropdown-divider
background-color: $dropdown-divider-background-color
border: none
display: block
height: 1px
margin: 0.5rem 0

View file

@ -0,0 +1,75 @@
.level
@extend %block
align-items: center
justify-content: space-between
code
border-radius: $radius
img
display: inline-block
vertical-align: top
// Modifiers
&.is-mobile
display: flex
.level-left,
.level-right
display: flex
.level-left + .level-right
margin-top: 0
.level-item
&:not(:last-child)
margin-bottom: 0
margin-right: 0.75rem
&:not(.is-narrow)
flex-grow: 1
// Responsiveness
+tablet
display: flex
& > .level-item
&:not(.is-narrow)
flex-grow: 1
.level-item
align-items: center
display: flex
flex-basis: auto
flex-grow: 0
flex-shrink: 0
justify-content: center
.title,
.subtitle
margin-bottom: 0
// Responsiveness
+mobile
&:not(:last-child)
margin-bottom: 0.75rem
.level-left,
.level-right
flex-basis: auto
flex-grow: 0
flex-shrink: 0
.level-item
// Modifiers
&.is-flexible
flex-grow: 1
// Responsiveness
+tablet
&:not(:last-child)
margin-right: 0.75rem
.level-left
align-items: center
justify-content: flex-start
// Responsiveness
+mobile
& + .level-right
margin-top: 1.5rem
+tablet
display: flex
.level-right
align-items: center
justify-content: flex-end
// Responsiveness
+tablet
display: flex

View file

@ -0,0 +1,39 @@
$list-background-color: $white !default
$list-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
$list-radius: $radius !default
$list-item-border: 1px solid $border !default
$list-item-color: $text !default
$list-item-active-background-color: $link !default
$list-item-active-color: $link-invert !default
$list-item-hover-background-color: $background !default
.list
@extend %block
background-color: $list-background-color
border-radius: $list-radius
box-shadow: $list-shadow
// &.is-hoverable > .list-item:hover:not(.is-active)
// background-color: $list-item-hover-background-color
// cursor: pointer
.list-item
display: block
padding: 0.5em 1em
&:not(a)
color: $list-item-color
&:first-child
border-top-left-radius: $list-radius
border-top-right-radius: $list-radius
&:last-child
border-top-left-radius: $list-radius
border-top-right-radius: $list-radius
&:not(:last-child)
border-bottom: $list-item-border
&.is-active
background-color: $list-item-active-background-color
color: $list-item-active-color
a.list-item
background-color: $list-item-hover-background-color
cursor: pointer

View file

@ -0,0 +1,48 @@
.media
align-items: flex-start
display: flex
text-align: left
.content:not(:last-child)
margin-bottom: 0.75rem
.media
border-top: 1px solid rgba($border, 0.5)
display: flex
padding-top: 0.75rem
.content:not(:last-child),
.control:not(:last-child)
margin-bottom: 0.5rem
.media
padding-top: 0.5rem
& + .media
margin-top: 0.5rem
& + .media
border-top: 1px solid rgba($border, 0.5)
margin-top: 1rem
padding-top: 1rem
// Sizes
&.is-large
& + .media
margin-top: 1.5rem
padding-top: 1.5rem
.media-left,
.media-right
flex-basis: auto
flex-grow: 0
flex-shrink: 0
.media-left
margin-right: 1rem
.media-right
margin-left: 1rem
.media-content
flex-basis: auto
flex-grow: 1
flex-shrink: 1
text-align: left
+mobile
.media-content
overflow-x: auto

View file

@ -0,0 +1,50 @@
$menu-item-color: $text !default
$menu-item-radius: $radius-small !default
$menu-item-hover-color: $text-strong !default
$menu-item-hover-background-color: $background !default
$menu-item-active-color: $link-invert !default
$menu-item-active-background-color: $link !default
$menu-list-border-left: 1px solid $border !default
$menu-label-color: $text-light !default
.menu
font-size: $size-normal
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
.menu-list
line-height: 1.25
a
border-radius: $menu-item-radius
color: $menu-item-color
display: block
padding: 0.5em 0.75em
&:hover
background-color: $menu-item-hover-background-color
color: $menu-item-hover-color
// Modifiers
&.is-active
background-color: $menu-item-active-background-color
color: $menu-item-active-color
li
ul
border-left: $menu-list-border-left
margin: 0.75em
padding-left: 0.75em
.menu-label
color: $menu-label-color
font-size: 0.75em
letter-spacing: 0.1em
text-transform: uppercase
&:not(:first-child)
margin-top: 1em
&:not(:last-child)
margin-bottom: 1em

View file

@ -0,0 +1,86 @@
$message-background-color: $background !default
$message-radius: $radius !default
$message-header-background-color: $text !default
$message-header-color: $text-invert !default
$message-header-weight: $weight-bold !default
$message-header-padding: 0.75em 1em !default
$message-header-radius: $radius !default
$message-body-border-color: $border !default
$message-body-border-width: 0 0 0 4px !default
$message-body-color: $text !default
$message-body-padding: 1.25em 1.5em !default
$message-body-radius: $radius !default
$message-body-pre-background-color: $white !default
$message-body-pre-code-background-color: transparent !default
$message-header-body-border-width: 0 !default
.message
@extend %block
background-color: $message-background-color
border-radius: $message-radius
font-size: $size-normal
strong
color: currentColor
a:not(.button):not(.tag):not(.dropdown-item)
color: currentColor
text-decoration: underline
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
$color-lightning: max((100% - lightness($color)) - 2%, 0%)
$color-luminance: colorLuminance($color)
$darken-percentage: $color-luminance * 70%
$desaturate-percentage: $color-luminance * 30%
&.is-#{$name}
background-color: lighten($color, $color-lightning)
.message-header
background-color: $color
color: $color-invert
.message-body
border-color: $color
color: desaturate(darken($color, $darken-percentage), $desaturate-percentage)
.message-header
align-items: center
background-color: $message-header-background-color
border-radius: $message-header-radius $message-header-radius 0 0
color: $message-header-color
display: flex
font-weight: $message-header-weight
justify-content: space-between
line-height: 1.25
padding: $message-header-padding
position: relative
.delete
flex-grow: 0
flex-shrink: 0
margin-left: 0.75em
& + .message-body
border-width: $message-header-body-border-width
border-top-left-radius: 0
border-top-right-radius: 0
.message-body
border-color: $message-body-border-color
border-radius: $message-body-radius
border-style: solid
border-width: $message-body-border-width
color: $message-body-color
padding: $message-body-padding
code,
pre
background-color: $message-body-pre-background-color
pre code
background-color: $message-body-pre-code-background-color

View file

@ -0,0 +1,113 @@
$modal-z: 40 !default
$modal-background-background-color: rgba($black, 0.86) !default
$modal-content-width: 640px !default
$modal-content-margin-mobile: 20px !default
$modal-content-spacing-mobile: 160px !default
$modal-content-spacing-tablet: 40px !default
$modal-close-dimensions: 40px !default
$modal-close-right: 20px !default
$modal-close-top: 20px !default
$modal-card-spacing: 40px !default
$modal-card-head-background-color: $background !default
$modal-card-head-border-bottom: 1px solid $border !default
$modal-card-head-padding: 20px !default
$modal-card-head-radius: $radius-large !default
$modal-card-title-color: $text-strong !default
$modal-card-title-line-height: 1 !default
$modal-card-title-size: $size-4 !default
$modal-card-foot-radius: $radius-large !default
$modal-card-foot-border-top: 1px solid $border !default
$modal-card-body-background-color: $white !default
$modal-card-body-padding: 20px !default
.modal
@extend %overlay
align-items: center
display: none
flex-direction: column
justify-content: center
overflow: hidden
position: fixed
z-index: $modal-z
// Modifiers
&.is-active
display: flex
.modal-background
@extend %overlay
background-color: $modal-background-background-color
.modal-content,
.modal-card
margin: 0 $modal-content-margin-mobile
max-height: calc(100vh - #{$modal-content-spacing-mobile})
overflow: auto
position: relative
width: 100%
// Responsiveness
+tablet
margin: 0 auto
max-height: calc(100vh - #{$modal-content-spacing-tablet})
width: $modal-content-width
.modal-close
@extend %delete
background: none
height: $modal-close-dimensions
position: fixed
right: $modal-close-right
top: $modal-close-top
width: $modal-close-dimensions
.modal-card
display: flex
flex-direction: column
max-height: calc(100vh - #{$modal-card-spacing})
overflow: hidden
-ms-overflow-y: visible
.modal-card-head,
.modal-card-foot
align-items: center
background-color: $modal-card-head-background-color
display: flex
flex-shrink: 0
justify-content: flex-start
padding: $modal-card-head-padding
position: relative
.modal-card-head
border-bottom: $modal-card-head-border-bottom
border-top-left-radius: $modal-card-head-radius
border-top-right-radius: $modal-card-head-radius
.modal-card-title
color: $modal-card-title-color
flex-grow: 1
flex-shrink: 0
font-size: $modal-card-title-size
line-height: $modal-card-title-line-height
.modal-card-foot
border-bottom-left-radius: $modal-card-foot-radius
border-bottom-right-radius: $modal-card-foot-radius
border-top: $modal-card-foot-border-top
.button
&:not(:last-child)
margin-right: 10px
.modal-card-body
+overflow-touch
background-color: $modal-card-body-background-color
flex-grow: 1
flex-shrink: 1
overflow: auto
padding: $modal-card-body-padding

View file

@ -0,0 +1,428 @@
$navbar-background-color: $white !default
$navbar-box-shadow-size: 0 2px 0 0 !default
$navbar-box-shadow-color: $background !default
$navbar-height: 3.25rem !default
$navbar-padding-vertical: 1rem !default
$navbar-padding-horizontal: 2rem !default
$navbar-z: 30 !default
$navbar-fixed-z: 30 !default
$navbar-item-color: $grey-dark !default
$navbar-item-hover-color: $link !default
$navbar-item-hover-background-color: $white-bis !default
$navbar-item-active-color: $black !default
$navbar-item-active-background-color: transparent !default
$navbar-item-img-max-height: 1.75rem !default
$navbar-burger-color: $navbar-item-color !default
$navbar-tab-hover-background-color: transparent !default
$navbar-tab-hover-border-bottom-color: $link !default
$navbar-tab-active-color: $link !default
$navbar-tab-active-background-color: transparent !default
$navbar-tab-active-border-bottom-color: $link !default
$navbar-tab-active-border-bottom-style: solid !default
$navbar-tab-active-border-bottom-width: 3px !default
$navbar-dropdown-background-color: $white !default
$navbar-dropdown-border-top: 2px solid $border !default
$navbar-dropdown-offset: -4px !default
$navbar-dropdown-arrow: $link !default
$navbar-dropdown-radius: $radius-large !default
$navbar-dropdown-z: 20 !default
$navbar-dropdown-boxed-radius: $radius-large !default
$navbar-dropdown-boxed-shadow: 0 8px 8px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
$navbar-dropdown-item-hover-color: $black !default
$navbar-dropdown-item-hover-background-color: $background !default
$navbar-dropdown-item-active-color: $link !default
$navbar-dropdown-item-active-background-color: $background !default
$navbar-divider-background-color: $background !default
$navbar-divider-height: 2px !default
$navbar-bottom-box-shadow-size: 0 -2px 0 0 !default
$navbar-breakpoint: $desktop !default
=navbar-fixed
left: 0
position: fixed
right: 0
z-index: $navbar-fixed-z
.navbar
background-color: $navbar-background-color
min-height: $navbar-height
position: relative
z-index: $navbar-z
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
color: $color-invert
.navbar-brand
& > .navbar-item,
.navbar-link
color: $color-invert
& > a.navbar-item,
.navbar-link
&:hover,
&.is-active
background-color: darken($color, 5%)
color: $color-invert
.navbar-link
&::after
border-color: $color-invert
.navbar-burger
color: $color-invert
+from($navbar-breakpoint)
.navbar-start,
.navbar-end
& > .navbar-item,
.navbar-link
color: $color-invert
& > a.navbar-item,
.navbar-link
&:hover,
&.is-active
background-color: darken($color, 5%)
color: $color-invert
.navbar-link
&::after
border-color: $color-invert
.navbar-item.has-dropdown:hover .navbar-link,
.navbar-item.has-dropdown.is-active .navbar-link
background-color: darken($color, 5%)
color: $color-invert
.navbar-dropdown
a.navbar-item
&.is-active
background-color: $color
color: $color-invert
& > .container
align-items: stretch
display: flex
min-height: $navbar-height
width: 100%
&.has-shadow
box-shadow: $navbar-box-shadow-size $navbar-box-shadow-color
&.is-fixed-bottom,
&.is-fixed-top
+navbar-fixed
&.is-fixed-bottom
bottom: 0
&.has-shadow
box-shadow: $navbar-bottom-box-shadow-size $navbar-box-shadow-color
&.is-fixed-top
top: 0
html,
body
&.has-navbar-fixed-top
padding-top: $navbar-height
&.has-navbar-fixed-bottom
padding-bottom: $navbar-height
.navbar-brand,
.navbar-tabs
align-items: stretch
display: flex
flex-shrink: 0
min-height: $navbar-height
.navbar-brand
a.navbar-item
&:hover
background-color: transparent
.navbar-tabs
+overflow-touch
max-width: 100vw
overflow-x: auto
overflow-y: hidden
.navbar-burger
color: $navbar-burger-color
+hamburger($navbar-height)
margin-left: auto
.navbar-menu
display: none
.navbar-item,
.navbar-link
color: $navbar-item-color
display: block
line-height: 1.5
padding: 0.5rem 0.75rem
position: relative
.icon
&:only-child
margin-left: -0.25rem
margin-right: -0.25rem
a.navbar-item,
.navbar-link
cursor: pointer
&:hover,
&.is-active
background-color: $navbar-item-hover-background-color
color: $navbar-item-hover-color
.navbar-item
display: block
flex-grow: 0
flex-shrink: 0
img
max-height: $navbar-item-img-max-height
&.has-dropdown
padding: 0
&.is-expanded
flex-grow: 1
flex-shrink: 1
&.is-tab
border-bottom: 1px solid transparent
min-height: $navbar-height
padding-bottom: calc(0.5rem - 1px)
&:hover
background-color: $navbar-tab-hover-background-color
border-bottom-color: $navbar-tab-hover-border-bottom-color
&.is-active
background-color: $navbar-tab-active-background-color
border-bottom-color: $navbar-tab-active-border-bottom-color
border-bottom-style: $navbar-tab-active-border-bottom-style
border-bottom-width: $navbar-tab-active-border-bottom-width
color: $navbar-tab-active-color
padding-bottom: calc(0.5rem - #{$navbar-tab-active-border-bottom-width})
.navbar-content
flex-grow: 1
flex-shrink: 1
.navbar-link:not(.is-arrowless)
padding-right: 2.5em
&::after
@extend %arrow
border-color: $navbar-dropdown-arrow
margin-top: -0.375em
right: 1.125em
.navbar-dropdown
font-size: 0.875rem
padding-bottom: 0.5rem
padding-top: 0.5rem
.navbar-item
padding-left: 1.5rem
padding-right: 1.5rem
.navbar-divider
background-color: $navbar-divider-background-color
border: none
display: none
height: $navbar-divider-height
margin: 0.5rem 0
+until($navbar-breakpoint)
.navbar > .container
display: block
.navbar-brand,
.navbar-tabs
.navbar-item
align-items: center
display: flex
.navbar-link
&::after
display: none
.navbar-menu
background-color: $navbar-background-color
box-shadow: 0 8px 16px rgba($black, 0.1)
padding: 0.5rem 0
&.is-active
display: block
// Fixed navbar
.navbar
&.is-fixed-bottom-touch,
&.is-fixed-top-touch
+navbar-fixed
&.is-fixed-bottom-touch
bottom: 0
&.has-shadow
box-shadow: 0 -2px 3px rgba($black, 0.1)
&.is-fixed-top-touch
top: 0
&.is-fixed-top,
&.is-fixed-top-touch
.navbar-menu
+overflow-touch
max-height: calc(100vh - #{$navbar-height})
overflow: auto
html,
body
&.has-navbar-fixed-top-touch
padding-top: $navbar-height
&.has-navbar-fixed-bottom-touch
padding-bottom: $navbar-height
+from($navbar-breakpoint)
.navbar,
.navbar-menu,
.navbar-start,
.navbar-end
align-items: stretch
display: flex
.navbar
min-height: $navbar-height
&.is-spaced
padding: $navbar-padding-vertical $navbar-padding-horizontal
.navbar-start,
.navbar-end
align-items: center
a.navbar-item,
.navbar-link
border-radius: $radius
&.is-transparent
a.navbar-item,
.navbar-link
&:hover,
&.is-active
background-color: transparent !important
.navbar-item.has-dropdown
&.is-active,
&.is-hoverable:hover
.navbar-link
background-color: transparent !important
.navbar-dropdown
a.navbar-item
&:hover
background-color: $navbar-dropdown-item-hover-background-color
color: $navbar-dropdown-item-hover-color
&.is-active
background-color: $navbar-dropdown-item-active-background-color
color: $navbar-dropdown-item-active-color
.navbar-burger
display: none
.navbar-item,
.navbar-link
align-items: center
display: flex
.navbar-item
display: flex
&.has-dropdown
align-items: stretch
&.has-dropdown-up
.navbar-link::after
transform: rotate(135deg) translate(0.25em, -0.25em)
.navbar-dropdown
border-bottom: $navbar-dropdown-border-top
border-radius: $navbar-dropdown-radius $navbar-dropdown-radius 0 0
border-top: none
bottom: 100%
box-shadow: 0 -8px 8px rgba($black, 0.1)
top: auto
&.is-active,
&.is-hoverable:hover
.navbar-dropdown
display: block
.navbar.is-spaced &,
&.is-boxed
opacity: 1
pointer-events: auto
transform: translateY(0)
.navbar-menu
flex-grow: 1
flex-shrink: 0
.navbar-start
justify-content: flex-start
margin-right: auto
.navbar-end
justify-content: flex-end
margin-left: auto
.navbar-dropdown
background-color: $navbar-dropdown-background-color
border-bottom-left-radius: $navbar-dropdown-radius
border-bottom-right-radius: $navbar-dropdown-radius
border-top: $navbar-dropdown-border-top
box-shadow: 0 8px 8px rgba($black, 0.1)
display: none
font-size: 0.875rem
left: 0
min-width: 100%
position: absolute
top: 100%
z-index: $navbar-dropdown-z
.navbar-item
padding: 0.375rem 1rem
white-space: nowrap
a.navbar-item
padding-right: 3rem
&:hover
background-color: $navbar-dropdown-item-hover-background-color
color: $navbar-dropdown-item-hover-color
&.is-active
background-color: $navbar-dropdown-item-active-background-color
color: $navbar-dropdown-item-active-color
.navbar.is-spaced &,
&.is-boxed
border-radius: $navbar-dropdown-boxed-radius
border-top: none
box-shadow: $navbar-dropdown-boxed-shadow
display: block
opacity: 0
pointer-events: none
top: calc(100% + (#{$navbar-dropdown-offset}))
transform: translateY(-5px)
transition-duration: $speed
transition-property: opacity, transform
&.is-right
left: auto
right: 0
.navbar-divider
display: block
.navbar > .container,
.container > .navbar
.navbar-brand
margin-left: -.75rem
.navbar-menu
margin-right: -.75rem
// Fixed navbar
.navbar
&.is-fixed-bottom-desktop,
&.is-fixed-top-desktop
+navbar-fixed
&.is-fixed-bottom-desktop
bottom: 0
&.has-shadow
box-shadow: 0 -2px 3px rgba($black, 0.1)
&.is-fixed-top-desktop
top: 0
html,
body
&.has-navbar-fixed-top-desktop
padding-top: $navbar-height
&.has-navbar-fixed-bottom-desktop
padding-bottom: $navbar-height
&.has-spaced-navbar-fixed-top
padding-top: $navbar-height + ($navbar-padding-vertical * 2)
&.has-spaced-navbar-fixed-bottom
padding-bottom: $navbar-height + ($navbar-padding-vertical * 2)
// Hover/Active states
a.navbar-item,
.navbar-link
&.is-active
color: $navbar-item-active-color
&.is-active:not(:hover)
background-color: $navbar-item-active-background-color
.navbar-item.has-dropdown
&:hover,
&.is-active
.navbar-link
background-color: $navbar-item-hover-background-color
// Combination
.hero
&.is-fullheight-with-navbar
min-height: calc(100vh - #{$navbar-height})

View file

@ -0,0 +1,144 @@
$pagination-color: $grey-darker !default
$pagination-border-color: $grey-lighter !default
$pagination-margin: -0.25rem !default
$pagination-min-width: $control-height !default
$pagination-hover-color: $link-hover !default
$pagination-hover-border-color: $link-hover-border !default
$pagination-focus-color: $link-focus !default
$pagination-focus-border-color: $link-focus-border !default
$pagination-active-color: $link-active !default
$pagination-active-border-color: $link-active-border !default
$pagination-disabled-color: $grey !default
$pagination-disabled-background-color: $grey-lighter !default
$pagination-disabled-border-color: $grey-lighter !default
$pagination-current-color: $link-invert !default
$pagination-current-background-color: $link !default
$pagination-current-border-color: $link !default
$pagination-ellipsis-color: $grey-light !default
$pagination-shadow-inset: inset 0 1px 2px rgba($black, 0.2)
.pagination
font-size: $size-normal
margin: $pagination-margin
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
&.is-rounded
.pagination-previous,
.pagination-next
padding-left: 1em
padding-right: 1em
border-radius: $radius-rounded
.pagination-link
border-radius: $radius-rounded
.pagination,
.pagination-list
align-items: center
display: flex
justify-content: center
text-align: center
.pagination-previous,
.pagination-next,
.pagination-link,
.pagination-ellipsis
@extend %control
@extend %unselectable
font-size: 1em
padding-left: 0.5em
padding-right: 0.5em
justify-content: center
margin: 0.25rem
text-align: center
.pagination-previous,
.pagination-next,
.pagination-link
border-color: $pagination-border-color
color: $pagination-color
min-width: $pagination-min-width
&:hover
border-color: $pagination-hover-border-color
color: $pagination-hover-color
&:focus
border-color: $pagination-focus-border-color
&:active
box-shadow: $pagination-shadow-inset
&[disabled]
background-color: $pagination-disabled-background-color
border-color: $pagination-disabled-border-color
box-shadow: none
color: $pagination-disabled-color
opacity: 0.5
.pagination-previous,
.pagination-next
padding-left: 0.75em
padding-right: 0.75em
white-space: nowrap
.pagination-link
&.is-current
background-color: $pagination-current-background-color
border-color: $pagination-current-border-color
color: $pagination-current-color
.pagination-ellipsis
color: $pagination-ellipsis-color
pointer-events: none
.pagination-list
flex-wrap: wrap
+mobile
.pagination
flex-wrap: wrap
.pagination-previous,
.pagination-next
flex-grow: 1
flex-shrink: 1
.pagination-list
li
flex-grow: 1
flex-shrink: 1
+tablet
.pagination-list
flex-grow: 1
flex-shrink: 1
justify-content: flex-start
order: 1
.pagination-previous
order: 2
.pagination-next
order: 3
.pagination
justify-content: space-between
&.is-centered
.pagination-previous
order: 1
.pagination-list
justify-content: center
order: 2
.pagination-next
order: 3
&.is-right
.pagination-previous
order: 1
.pagination-next
order: 2
.pagination-list
justify-content: flex-end
order: 3

View file

@ -0,0 +1,101 @@
$panel-item-border: 1px solid $border !default
$panel-heading-background-color: $background !default
$panel-heading-color: $text-strong !default
$panel-heading-line-height: 1.25 !default
$panel-heading-padding: 0.5em 0.75em !default
$panel-heading-radius: $radius !default
$panel-heading-size: 1.25em !default
$panel-heading-weight: $weight-light !default
$panel-tab-border-bottom: 1px solid $border !default
$panel-tab-active-border-bottom-color: $link-active-border !default
$panel-tab-active-color: $link-active !default
$panel-list-item-color: $text !default
$panel-list-item-hover-color: $link !default
$panel-block-color: $text-strong !default
$panel-block-hover-background-color: $background !default
$panel-block-active-border-left-color: $link !default
$panel-block-active-color: $link-active !default
$panel-block-active-icon-color: $link !default
$panel-icon-color: $text-light !default
.panel
font-size: $size-normal
&:not(:last-child)
margin-bottom: 1.5rem
.panel-heading,
.panel-tabs,
.panel-block
border-bottom: $panel-item-border
border-left: $panel-item-border
border-right: $panel-item-border
&:first-child
border-top: $panel-item-border
.panel-heading
background-color: $panel-heading-background-color
border-radius: $panel-heading-radius $panel-heading-radius 0 0
color: $panel-heading-color
font-size: $panel-heading-size
font-weight: $panel-heading-weight
line-height: $panel-heading-line-height
padding: $panel-heading-padding
.panel-tabs
align-items: flex-end
display: flex
font-size: 0.875em
justify-content: center
a
border-bottom: $panel-tab-border-bottom
margin-bottom: -1px
padding: 0.5em
// Modifiers
&.is-active
border-bottom-color: $panel-tab-active-border-bottom-color
color: $panel-tab-active-color
.panel-list
a
color: $panel-list-item-color
&:hover
color: $panel-list-item-hover-color
.panel-block
align-items: center
color: $panel-block-color
display: flex
justify-content: flex-start
padding: 0.5em 0.75em
input[type="checkbox"]
margin-right: 0.75em
& > .control
flex-grow: 1
flex-shrink: 1
width: 100%
&.is-wrapped
flex-wrap: wrap
&.is-active
border-left-color: $panel-block-active-border-left-color
color: $panel-block-active-color
.panel-icon
color: $panel-block-active-icon-color
a.panel-block,
label.panel-block
cursor: pointer
&:hover
background-color: $panel-block-hover-background-color
.panel-icon
+fa(14px, 1em)
color: $panel-icon-color
margin-right: 0.75em
.fa
font-size: inherit
line-height: inherit

View file

@ -0,0 +1,151 @@
$tabs-border-bottom-color: $border !default
$tabs-border-bottom-style: solid !default
$tabs-border-bottom-width: 1px !default
$tabs-link-color: $text !default
$tabs-link-hover-border-bottom-color: $text-strong !default
$tabs-link-hover-color: $text-strong !default
$tabs-link-active-border-bottom-color: $link !default
$tabs-link-active-color: $link !default
$tabs-link-padding: 0.5em 1em !default
$tabs-boxed-link-radius: $radius !default
$tabs-boxed-link-hover-background-color: $background !default
$tabs-boxed-link-hover-border-bottom-color: $border !default
$tabs-boxed-link-active-background-color: $white !default
$tabs-boxed-link-active-border-color: $border !default
$tabs-boxed-link-active-border-bottom-color: transparent !default
$tabs-toggle-link-border-color: $border !default
$tabs-toggle-link-border-style: solid !default
$tabs-toggle-link-border-width: 1px !default
$tabs-toggle-link-hover-background-color: $background !default
$tabs-toggle-link-hover-border-color: $border-hover !default
$tabs-toggle-link-radius: $radius !default
$tabs-toggle-link-active-background-color: $link !default
$tabs-toggle-link-active-border-color: $link !default
$tabs-toggle-link-active-color: $link-invert !default
.tabs
@extend %block
+overflow-touch
@extend %unselectable
align-items: stretch
display: flex
font-size: $size-normal
justify-content: space-between
overflow: hidden
overflow-x: auto
white-space: nowrap
a
align-items: center
border-bottom-color: $tabs-border-bottom-color
border-bottom-style: $tabs-border-bottom-style
border-bottom-width: $tabs-border-bottom-width
color: $tabs-link-color
display: flex
justify-content: center
margin-bottom: -#{$tabs-border-bottom-width}
padding: $tabs-link-padding
vertical-align: top
&:hover
border-bottom-color: $tabs-link-hover-border-bottom-color
color: $tabs-link-hover-color
li
display: block
&.is-active
a
border-bottom-color: $tabs-link-active-border-bottom-color
color: $tabs-link-active-color
ul
align-items: center
border-bottom-color: $tabs-border-bottom-color
border-bottom-style: $tabs-border-bottom-style
border-bottom-width: $tabs-border-bottom-width
display: flex
flex-grow: 1
flex-shrink: 0
justify-content: flex-start
&.is-left
padding-right: 0.75em
&.is-center
flex: none
justify-content: center
padding-left: 0.75em
padding-right: 0.75em
&.is-right
justify-content: flex-end
padding-left: 0.75em
.icon
&:first-child
margin-right: 0.5em
&:last-child
margin-left: 0.5em
// Alignment
&.is-centered
ul
justify-content: center
&.is-right
ul
justify-content: flex-end
// Styles
&.is-boxed
a
border: 1px solid transparent
border-radius: $tabs-boxed-link-radius $tabs-boxed-link-radius 0 0
&:hover
background-color: $tabs-boxed-link-hover-background-color
border-bottom-color: $tabs-boxed-link-hover-border-bottom-color
li
&.is-active
a
background-color: $tabs-boxed-link-active-background-color
border-color: $tabs-boxed-link-active-border-color
border-bottom-color: $tabs-boxed-link-active-border-bottom-color !important
&.is-fullwidth
li
flex-grow: 1
flex-shrink: 0
&.is-toggle
a
border-color: $tabs-toggle-link-border-color
border-style: $tabs-toggle-link-border-style
border-width: $tabs-toggle-link-border-width
margin-bottom: 0
position: relative
&:hover
background-color: $tabs-toggle-link-hover-background-color
border-color: $tabs-toggle-link-hover-border-color
z-index: 2
li
& + li
margin-left: -#{$tabs-toggle-link-border-width}
&:first-child a
border-radius: $tabs-toggle-link-radius 0 0 $tabs-toggle-link-radius
&:last-child a
border-radius: 0 $tabs-toggle-link-radius $tabs-toggle-link-radius 0
&.is-active
a
background-color: $tabs-toggle-link-active-background-color
border-color: $tabs-toggle-link-active-border-color
color: $tabs-toggle-link-active-color
z-index: 1
ul
border-bottom: none
&.is-toggle-rounded
li
&:first-child a
border-bottom-left-radius: $radius-rounded
border-top-left-radius: $radius-rounded
padding-left: 1.25em
&:last-child a
border-bottom-right-radius: $radius-rounded
border-top-right-radius: $radius-rounded
padding-right: 1.25em
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large

View file

@ -0,0 +1,60 @@
// Custom Elements
.hero.lg-hero-bg
background-color: $lg-hero-bg
color: findColorInvert($lg-hero-bg)
.hero-foot.lg-hero-bg
background-color: $lg-hero-bg
color: findColorInvert($lg-hero-bg)
.hero.lg-hero-bg .title, .subtitle
color: findColorInvert($lg-hero-bg)
.hero.is-danger .title, .subtitle
color: findColorInvert($danger)
.hero.lg-hero-bg .navbar-item
color: findColorInvert($lg-hero-bg)
.button.lg-btn-submit
background-color: $lg-btn-submit
border-color: transparent
color: findColorInvert($lg-btn-submit)
.tag.lg-tag-loctitle
background-color: $lg-tag-loctitle
color: findColorInvert($lg-tag-loctitle)
.tag.lg-tag-cmd
background-color: $lg-tag-cmd
color: findColorInvert($lg-tag-cmd)
font-family: $family-monospace
.tag.lg-tag-cmdtitle
background-color: $lg-tag-cmdtitle
color: findColorInvert($lg-tag-cmdtitle)
font-family: $family-monospace
.tag.lg-tag-loc
background-color: $lg-tag-loc
color: findColorInvert($lg-tag-loc)
.progress.lg-progressbar:indeterminate
background-image: linear-gradient(to right,$lg-progressbar 30%,$grey-lighter 30%)
.message-body pre
background-color: transparent!important
.query-output
font-family: $family-monospace
font-size: 0.875em
overflow-x: auto
white-space: pre
word-wrap: normal
.dropdown-content.lg-help
width: 24rem
.icon.lg-icon-help
color: $lg-btn-submit

View file

@ -0,0 +1,16 @@
@charset "utf-8"
@import "box.sass"
@import "button.sass"
@import "container.sass"
@import "content.sass"
@import "form.sass"
@import "icon.sass"
@import "image.sass"
@import "notification.sass"
@import "progress.sass"
@import "table.sass"
@import "tag.sass"
@import "title.sass"
@import "other.sass"

View file

@ -0,0 +1,24 @@
$box-color: $text !default
$box-background-color: $white !default
$box-radius: $radius-large !default
$box-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
$box-padding: 1.25rem !default
$box-link-hover-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px $link !default
$box-link-active-shadow: inset 0 1px 2px rgba($black, 0.2), 0 0 0 1px $link !default
.box
@extend %block
background-color: $box-background-color
border-radius: $box-radius
box-shadow: $box-shadow
color: $box-color
display: block
padding: $box-padding
a.box
&:hover,
&:focus
box-shadow: $box-link-hover-shadow
&:active
box-shadow: $box-link-active-shadow

View file

@ -0,0 +1,275 @@
$button-color: $grey-darker !default
$button-background-color: $white !default
$button-border-color: $grey-lighter !default
$button-border-width: $control-border-width !default
$button-padding-vertical: calc(0.375em - #{$button-border-width}) !default
$button-padding-horizontal: 0.75em !default
$button-hover-color: $link-hover !default
$button-hover-border-color: $link-hover-border !default
$button-focus-color: $link-focus !default
$button-focus-border-color: $link-focus-border !default
$button-focus-box-shadow-size: 0 0 0 0.125em !default
$button-focus-box-shadow-color: rgba($link, 0.25) !default
$button-active-color: $link-active !default
$button-active-border-color: $link-active-border !default
$button-text-color: $text !default
$button-text-hover-background-color: $background !default
$button-text-hover-color: $text-strong !default
$button-disabled-background-color: $white !default
$button-disabled-border-color: $grey-lighter !default
$button-disabled-shadow: none !default
$button-disabled-opacity: 0.5 !default
$button-static-color: $grey !default
$button-static-background-color: $white-ter !default
$button-static-border-color: $grey-lighter !default
// The button sizes use mixins so they can be used at different breakpoints
=button-small
border-radius: $radius-small
font-size: $size-small
=button-normal
font-size: $size-normal
=button-medium
font-size: $size-medium
=button-large
font-size: $size-large
.button
@extend %control
@extend %unselectable
background-color: $button-background-color
border-color: $button-border-color
border-width: $button-border-width
color: $button-color
cursor: pointer
justify-content: center
padding-bottom: $button-padding-vertical
padding-left: $button-padding-horizontal
padding-right: $button-padding-horizontal
padding-top: $button-padding-vertical
text-align: center
white-space: nowrap
strong
color: inherit
.icon
&,
&.is-small,
&.is-medium,
&.is-large
height: 1.5em
width: 1.5em
&:first-child:not(:last-child)
margin-left: calc(-0.375em - #{$button-border-width})
margin-right: 0.1875em
&:last-child:not(:first-child)
margin-left: 0.1875em
margin-right: calc(-0.375em - #{$button-border-width})
&:first-child:last-child
margin-left: calc(-0.375em - #{$button-border-width})
margin-right: calc(-0.375em - #{$button-border-width})
// States
&:hover,
&.is-hovered
border-color: $button-hover-border-color
color: $button-hover-color
&:focus,
&.is-focused
border-color: $button-focus-border-color
color: $button-focus-color
&:not(:active)
box-shadow: $button-focus-box-shadow-size $button-focus-box-shadow-color
&:active,
&.is-active
border-color: $button-active-border-color
color: $button-active-color
// Colors
&.is-text
background-color: transparent
border-color: transparent
color: $button-text-color
text-decoration: underline
&:hover,
&.is-hovered,
&:focus,
&.is-focused
background-color: $button-text-hover-background-color
color: $button-text-hover-color
&:active,
&.is-active
background-color: darken($button-text-hover-background-color, 5%)
color: $button-text-hover-color
&[disabled],
fieldset[disabled] &
background-color: transparent
border-color: transparent
box-shadow: none
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
border-color: transparent
color: $color-invert
&:hover,
&.is-hovered
background-color: darken($color, 2.5%)
border-color: transparent
color: $color-invert
&:focus,
&.is-focused
border-color: transparent
color: $color-invert
&:not(:active)
box-shadow: $button-focus-box-shadow-size rgba($color, 0.25)
&:active,
&.is-active
background-color: darken($color, 5%)
border-color: transparent
color: $color-invert
&[disabled],
fieldset[disabled] &
background-color: $color
border-color: transparent
box-shadow: none
&.is-inverted
background-color: $color-invert
color: $color
&:hover
background-color: darken($color-invert, 5%)
&[disabled],
fieldset[disabled] &
background-color: $color-invert
border-color: transparent
box-shadow: none
color: $color
&.is-loading
&::after
border-color: transparent transparent $color-invert $color-invert !important
&.is-outlined
background-color: transparent
border-color: $color
color: $color
&:hover,
&:focus
background-color: $color
border-color: $color
color: $color-invert
&.is-loading
&::after
border-color: transparent transparent $color $color !important
&[disabled],
fieldset[disabled] &
background-color: transparent
border-color: $color
box-shadow: none
color: $color
&.is-inverted.is-outlined
background-color: transparent
border-color: $color-invert
color: $color-invert
&:hover,
&:focus
background-color: $color-invert
color: $color
&[disabled],
fieldset[disabled] &
background-color: transparent
border-color: $color-invert
box-shadow: none
color: $color-invert
// Sizes
&.is-small
+button-small
&.is-normal
+button-normal
&.is-medium
+button-medium
&.is-large
+button-large
// Modifiers
&[disabled],
fieldset[disabled] &
background-color: $button-disabled-background-color
border-color: $button-disabled-border-color
box-shadow: $button-disabled-shadow
opacity: $button-disabled-opacity
&.is-fullwidth
display: flex
width: 100%
&.is-loading
color: transparent !important
pointer-events: none
&::after
@extend %loader
+center(1em)
position: absolute !important
&.is-static
background-color: $button-static-background-color
border-color: $button-static-border-color
color: $button-static-color
box-shadow: none
pointer-events: none
&.is-rounded
border-radius: $radius-rounded
padding-left: 1em
padding-right: 1em
.buttons
align-items: center
display: flex
flex-wrap: wrap
justify-content: flex-start
.button
margin-bottom: 0.5rem
&:not(:last-child):not(.is-fullwidth)
margin-right: 0.5rem
&:last-child
margin-bottom: -0.5rem
&:not(:last-child)
margin-bottom: 1rem
// Sizes
&.are-small
.button:not(.is-normal):not(.is-medium):not(.is-large)
+button-small
&.are-medium
.button:not(.is-small):not(.is-normal):not(.is-large)
+button-medium
&.are-large
.button:not(.is-small):not(.is-normal):not(.is-medium)
+button-large
&.has-addons
.button
&:not(:first-child)
border-bottom-left-radius: 0
border-top-left-radius: 0
&:not(:last-child)
border-bottom-right-radius: 0
border-top-right-radius: 0
margin-right: -1px
&:last-child
margin-right: 0
&:hover,
&.is-hovered
z-index: 2
&:focus,
&.is-focused,
&:active,
&.is-active,
&.is-selected
z-index: 3
&:hover
z-index: 4
&.is-expanded
flex-grow: 1
&.is-centered
justify-content: center
&.is-right
justify-content: flex-end

View file

@ -0,0 +1,25 @@
.container
margin: 0 auto
position: relative
+desktop
max-width: $desktop - (2 * $gap)
width: $desktop - (2 * $gap)
&.is-fluid
margin-left: $gap
margin-right: $gap
max-width: none
width: auto
+until-widescreen
&.is-widescreen
max-width: $widescreen - (2 * $gap)
width: auto
+until-fullhd
&.is-fullhd
max-width: $fullhd - (2 * $gap)
width: auto
+widescreen
max-width: $widescreen - (2 * $gap)
width: $widescreen - (2 * $gap)
+fullhd
max-width: $fullhd - (2 * $gap)
width: $fullhd - (2 * $gap)

View file

@ -0,0 +1,151 @@
$content-heading-color: $text-strong !default
$content-heading-weight: $weight-semibold !default
$content-heading-line-height: 1.125 !default
$content-blockquote-background-color: $background !default
$content-blockquote-border-left: 5px solid $border !default
$content-blockquote-padding: 1.25em 1.5em !default
$content-pre-padding: 1.25em 1.5em !default
$content-table-cell-border: 1px solid $border !default
$content-table-cell-border-width: 0 0 1px !default
$content-table-cell-padding: 0.5em 0.75em !default
$content-table-cell-heading-color: $text-strong !default
$content-table-head-cell-border-width: 0 0 2px !default
$content-table-head-cell-color: $text-strong !default
$content-table-foot-cell-border-width: 2px 0 0 !default
$content-table-foot-cell-color: $text-strong !default
.content
@extend %block
// Inline
li + li
margin-top: 0.25em
// Block
p,
dl,
ol,
ul,
blockquote,
pre,
table
&:not(:last-child)
margin-bottom: 1em
h1,
h2,
h3,
h4,
h5,
h6
color: $content-heading-color
font-weight: $content-heading-weight
line-height: $content-heading-line-height
h1
font-size: 2em
margin-bottom: 0.5em
&:not(:first-child)
margin-top: 1em
h2
font-size: 1.75em
margin-bottom: 0.5714em
&:not(:first-child)
margin-top: 1.1428em
h3
font-size: 1.5em
margin-bottom: 0.6666em
&:not(:first-child)
margin-top: 1.3333em
h4
font-size: 1.25em
margin-bottom: 0.8em
h5
font-size: 1.125em
margin-bottom: 0.8888em
h6
font-size: 1em
margin-bottom: 1em
blockquote
background-color: $content-blockquote-background-color
border-left: $content-blockquote-border-left
padding: $content-blockquote-padding
ol
list-style-position: outside
margin-left: 2em
margin-top: 1em
&:not([type])
list-style-type: decimal
&.is-lower-alpha
list-style-type: lower-alpha
&.is-lower-roman
list-style-type: lower-roman
&.is-upper-alpha
list-style-type: upper-alpha
&.is-upper-roman
list-style-type: upper-roman
ul
list-style: disc outside
margin-left: 2em
margin-top: 1em
ul
list-style-type: circle
margin-top: 0.5em
ul
list-style-type: square
dd
margin-left: 2em
figure
margin-left: 2em
margin-right: 2em
text-align: center
&:not(:first-child)
margin-top: 2em
&:not(:last-child)
margin-bottom: 2em
img
display: inline-block
figcaption
font-style: italic
pre
+overflow-touch
overflow-x: auto
padding: $content-pre-padding
white-space: pre
word-wrap: normal
sup,
sub
font-size: 75%
table
width: 100%
td,
th
border: $content-table-cell-border
border-width: $content-table-cell-border-width
padding: $content-table-cell-padding
vertical-align: top
th
color: $content-table-cell-heading-color
text-align: left
thead
td,
th
border-width: $content-table-head-cell-border-width
color: $content-table-head-cell-color
tfoot
td,
th
border-width: $content-table-foot-cell-border-width
color: $content-table-foot-cell-color
tbody
tr
&:last-child
td,
th
border-bottom-width: 0
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large

View file

@ -0,0 +1,602 @@
$input-color: $grey-darker !default
$input-background-color: $white !default
$input-border-color: $grey-lighter !default
$input-height: $control-height !default
$input-shadow: inset 0 1px 2px rgba($black, 0.1) !default
$input-placeholder-color: rgba($input-color, 0.3) !default
$input-hover-color: $grey-darker !default
$input-hover-border-color: $grey-light !default
$input-focus-color: $grey-darker !default
$input-focus-border-color: $link !default
$input-focus-box-shadow-size: 0 0 0 0.125em !default
$input-focus-box-shadow-color: rgba($link, 0.25) !default
$input-disabled-color: $text-light !default
$input-disabled-background-color: $background !default
$input-disabled-border-color: $background !default
$input-disabled-placeholder-color: rgba($input-disabled-color, 0.3) !default
$input-arrow: $link !default
$input-icon-color: $grey-lighter !default
$input-icon-active-color: $grey !default
$input-radius: $radius !default
$file-border-color: $border !default
$file-radius: $radius !default
$file-cta-background-color: $white-ter !default
$file-cta-color: $grey-dark !default
$file-cta-hover-color: $grey-darker !default
$file-cta-active-color: $grey-darker !default
$file-name-border-color: $border !default
$file-name-border-style: solid !default
$file-name-border-width: 1px 1px 1px 0 !default
$file-name-max-width: 16em !default
$label-color: $grey-darker !default
$label-weight: $weight-bold !default
$help-size: $size-small !default
=input
@extend %control
background-color: $input-background-color
border-color: $input-border-color
color: $input-color
+placeholder
color: $input-placeholder-color
&:hover,
&.is-hovered
border-color: $input-hover-border-color
&:focus,
&.is-focused,
&:active,
&.is-active
border-color: $input-focus-border-color
box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color
&[disabled],
fieldset[disabled] &
background-color: $input-disabled-background-color
border-color: $input-disabled-border-color
box-shadow: none
color: $input-disabled-color
+placeholder
color: $input-disabled-placeholder-color
.input,
.textarea
+input
box-shadow: $input-shadow
max-width: 100%
width: 100%
&[readonly]
box-shadow: none
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
&.is-#{$name}
border-color: $color
&:focus,
&.is-focused,
&:active,
&.is-active
box-shadow: $input-focus-box-shadow-size rgba($color, 0.25)
// Sizes
&.is-small
+control-small
&.is-medium
+control-medium
&.is-large
+control-large
// Modifiers
&.is-fullwidth
display: block
width: 100%
&.is-inline
display: inline
width: auto
.input
&.is-rounded
border-radius: $radius-rounded
padding-left: 1em
padding-right: 1em
&.is-static
background-color: transparent
border-color: transparent
box-shadow: none
padding-left: 0
padding-right: 0
.textarea
display: block
max-width: 100%
min-width: 100%
padding: 0.625em
resize: vertical
&:not([rows])
max-height: 600px
min-height: 120px
&[rows]
height: initial
// Modifiers
&.has-fixed-size
resize: none
.checkbox,
.radio
cursor: pointer
display: inline-block
line-height: 1.25
position: relative
input
cursor: pointer
&:hover
color: $input-hover-color
&[disabled],
fieldset[disabled] &
color: $input-disabled-color
cursor: not-allowed
.radio
& + .radio
margin-left: 0.5em
.select
display: inline-block
max-width: 100%
position: relative
vertical-align: top
&:not(.is-multiple)
height: $input-height
&:not(.is-multiple):not(.is-loading)
&::after
@extend %arrow
border-color: $input-arrow
right: 1.125em
z-index: 4
&.is-rounded
select
border-radius: $radius-rounded
padding-left: 1em
select
+input
cursor: pointer
display: block
font-size: 1em
max-width: 100%
outline: none
&::-ms-expand
display: none
&[disabled]:hover,
fieldset[disabled] &:hover
border-color: $input-disabled-border-color
&:not([multiple])
padding-right: 2.5em
&[multiple]
height: auto
padding: 0
option
padding: 0.5em 1em
// States
&:not(.is-multiple):not(.is-loading):hover
&::after
border-color: $input-hover-color
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
&.is-#{$name}
&:not(:hover)::after
border-color: $color
select
border-color: $color
&:hover,
&.is-hovered
border-color: darken($color, 5%)
&:focus,
&.is-focused,
&:active,
&.is-active
box-shadow: $input-focus-box-shadow-size rgba($color, 0.25)
// Sizes
&.is-small
+control-small
&.is-medium
+control-medium
&.is-large
+control-large
// Modifiers
&.is-disabled
&::after
border-color: $input-disabled-color
&.is-fullwidth
width: 100%
select
width: 100%
&.is-loading
&::after
@extend %loader
margin-top: 0
position: absolute
right: 0.625em
top: 0.625em
transform: none
&.is-small:after
font-size: $size-small
&.is-medium:after
font-size: $size-medium
&.is-large:after
font-size: $size-large
.file
@extend %unselectable
align-items: stretch
display: flex
justify-content: flex-start
position: relative
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
.file-cta
background-color: $color
border-color: transparent
color: $color-invert
&:hover,
&.is-hovered
.file-cta
background-color: darken($color, 2.5%)
border-color: transparent
color: $color-invert
&:focus,
&.is-focused
.file-cta
border-color: transparent
box-shadow: 0 0 0.5em rgba($color, 0.25)
color: $color-invert
&:active,
&.is-active
.file-cta
background-color: darken($color, 5%)
border-color: transparent
color: $color-invert
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
.file-icon
.fa
font-size: 21px
&.is-large
font-size: $size-large
.file-icon
.fa
font-size: 28px
// Modifiers
&.has-name
.file-cta
border-bottom-right-radius: 0
border-top-right-radius: 0
.file-name
border-bottom-left-radius: 0
border-top-left-radius: 0
&.is-empty
.file-cta
border-radius: $file-radius
.file-name
display: none
&.is-boxed
.file-label
flex-direction: column
.file-cta
flex-direction: column
height: auto
padding: 1em 3em
.file-name
border-width: 0 1px 1px
.file-icon
height: 1.5em
width: 1.5em
.fa
font-size: 21px
&.is-small
.file-icon .fa
font-size: 14px
&.is-medium
.file-icon .fa
font-size: 28px
&.is-large
.file-icon .fa
font-size: 35px
&.has-name
.file-cta
border-radius: $file-radius $file-radius 0 0
.file-name
border-radius: 0 0 $file-radius $file-radius
border-width: 0 1px 1px
&.is-centered
justify-content: center
&.is-fullwidth
.file-label
width: 100%
.file-name
flex-grow: 1
max-width: none
&.is-right
justify-content: flex-end
.file-cta
border-radius: 0 $file-radius $file-radius 0
.file-name
border-radius: $file-radius 0 0 $file-radius
border-width: 1px 0 1px 1px
order: -1
.file-label
align-items: stretch
display: flex
cursor: pointer
justify-content: flex-start
overflow: hidden
position: relative
&:hover
.file-cta
background-color: darken($file-cta-background-color, 2.5%)
color: $file-cta-hover-color
.file-name
border-color: darken($file-name-border-color, 2.5%)
&:active
.file-cta
background-color: darken($file-cta-background-color, 5%)
color: $file-cta-active-color
.file-name
border-color: darken($file-name-border-color, 5%)
.file-input
height: 100%
left: 0
opacity: 0
outline: none
position: absolute
top: 0
width: 100%
.file-cta,
.file-name
@extend %control
border-color: $file-border-color
border-radius: $file-radius
font-size: 1em
padding-left: 1em
padding-right: 1em
white-space: nowrap
.file-cta
background-color: $file-cta-background-color
color: $file-cta-color
.file-name
border-color: $file-name-border-color
border-style: $file-name-border-style
border-width: $file-name-border-width
display: block
max-width: $file-name-max-width
overflow: hidden
text-align: left
text-overflow: ellipsis
.file-icon
align-items: center
display: flex
height: 1em
justify-content: center
margin-right: 0.5em
width: 1em
.fa
font-size: 14px
.label
color: $label-color
display: block
font-size: $size-normal
font-weight: $label-weight
&:not(:last-child)
margin-bottom: 0.5em
// Sizes
&.is-small
font-size: $size-small
&.is-medium
font-size: $size-medium
&.is-large
font-size: $size-large
.help
display: block
font-size: $help-size
margin-top: 0.25rem
@each $name, $pair in $colors
$color: nth($pair, 1)
&.is-#{$name}
color: $color
// Containers
.field
&:not(:last-child)
margin-bottom: 0.75rem
// Modifiers
&.has-addons
display: flex
justify-content: flex-start
.control
&:not(:last-child)
margin-right: -1px
&:not(:first-child):not(:last-child)
.button,
.input,
.select select
border-radius: 0
&:first-child:not(:only-child)
.button,
.input,
.select select
border-bottom-right-radius: 0
border-top-right-radius: 0
&:last-child:not(:only-child)
.button,
.input,
.select select
border-bottom-left-radius: 0
border-top-left-radius: 0
.button,
.input,
.select select
&:not([disabled])
&:hover,
&.is-hovered
z-index: 2
&:focus,
&.is-focused,
&:active,
&.is-active
z-index: 3
&:hover
z-index: 4
&.is-expanded
flex-grow: 1
&.has-addons-centered
justify-content: center
&.has-addons-right
justify-content: flex-end
&.has-addons-fullwidth
.control
flex-grow: 1
flex-shrink: 0
&.is-grouped
display: flex
justify-content: flex-start
& > .control
flex-shrink: 0
&:not(:last-child)
margin-bottom: 0
margin-right: 0.75rem
&.is-expanded
flex-grow: 1
flex-shrink: 1
&.is-grouped-centered
justify-content: center
&.is-grouped-right
justify-content: flex-end
&.is-grouped-multiline
flex-wrap: wrap
& > .control
&:last-child,
&:not(:last-child)
margin-bottom: 0.75rem
&:last-child
margin-bottom: -0.75rem
&:not(:last-child)
margin-bottom: 0
&.is-horizontal
+tablet
display: flex
.field-label
.label
font-size: inherit
+mobile
margin-bottom: 0.5rem
+tablet
flex-basis: 0
flex-grow: 1
flex-shrink: 0
margin-right: 1.5rem
text-align: right
&.is-small
font-size: $size-small
padding-top: 0.375em
&.is-normal
padding-top: 0.375em
&.is-medium
font-size: $size-medium
padding-top: 0.375em
&.is-large
font-size: $size-large
padding-top: 0.375em
.field-body
.field .field
margin-bottom: 0
+tablet
display: flex
flex-basis: 0
flex-grow: 5
flex-shrink: 1
.field
margin-bottom: 0
& > .field
flex-shrink: 1
&:not(.is-narrow)
flex-grow: 1
&:not(:last-child)
margin-right: 0.75rem
.control
box-sizing: border-box
clear: both //fixes the icon floating out of the input when help text is floated right
font-size: $size-normal
position: relative
text-align: left
// Modifiers
&.has-icons-left,
&.has-icons-right
.input,
.select
&:focus
& ~ .icon
color: $input-icon-active-color
&.is-small ~ .icon
font-size: $size-small
&.is-medium ~ .icon
font-size: $size-medium
&.is-large ~ .icon
font-size: $size-large
.icon
color: $input-icon-color
height: $input-height
pointer-events: none
position: absolute
top: 0
width: $input-height
z-index: 4
&.has-icons-left
.input,
.select select
padding-left: $input-height
.icon.is-left
left: 0
&.has-icons-right
.input,
.select select
padding-right: $input-height
.icon.is-right
right: 0
&.is-loading
&::after
@extend %loader
position: absolute !important
right: 0.625em
top: 0.625em
z-index: 4
&.is-small:after
font-size: $size-small
&.is-medium:after
font-size: $size-medium
&.is-large:after
font-size: $size-large

View file

@ -0,0 +1,21 @@
$icon-dimensions: 1.5rem !default
$icon-dimensions-small: 1rem !default
$icon-dimensions-medium: 2rem !default
$icon-dimensions-large: 3rem !default
.icon
align-items: center
display: inline-flex
justify-content: center
height: $icon-dimensions
width: $icon-dimensions
// Sizes
&.is-small
height: $icon-dimensions-small
width: $icon-dimensions-small
&.is-medium
height: $icon-dimensions-medium
width: $icon-dimensions-medium
&.is-large
height: $icon-dimensions-large
width: $icon-dimensions-large

View file

@ -0,0 +1,69 @@
$dimensions: 16 24 32 48 64 96 128 !default
.image
display: block
position: relative
img
display: block
height: auto
width: 100%
&.is-rounded
border-radius: $radius-rounded
// Ratio
&.is-square,
&.is-1by1,
&.is-5by4,
&.is-4by3,
&.is-3by2,
&.is-5by3,
&.is-16by9,
&.is-2by1,
&.is-3by1,
&.is-4by5,
&.is-3by4,
&.is-2by3,
&.is-3by5,
&.is-9by16,
&.is-1by2,
&.is-1by3
img,
.has-ratio
@extend %overlay
height: 100%
width: 100%
&.is-square,
&.is-1by1
padding-top: 100%
&.is-5by4
padding-top: 80%
&.is-4by3
padding-top: 75%
&.is-3by2
padding-top: 66.6666%
&.is-5by3
padding-top: 60%
&.is-16by9
padding-top: 56.25%
&.is-2by1
padding-top: 50%
&.is-3by1
padding-top: 33.3333%
&.is-4by5
padding-top: 125%
&.is-3by4
padding-top: 133.3333%
&.is-2by3
padding-top: 150%
&.is-3by5
padding-top: 166.6666%
&.is-9by16
padding-top: 177.7777%
&.is-1by2
padding-top: 200%
&.is-1by3
padding-top: 300%
// Sizes
@each $dimension in $dimensions
&.is-#{$dimension}x#{$dimension}
height: $dimension * 1px
width: $dimension * 1px

View file

@ -0,0 +1,35 @@
$notification-background-color: $background !default
$notification-radius: $radius !default
$notification-padding: 1.25rem 2.5rem 1.25rem 1.5rem !default
.notification
@extend %block
background-color: $notification-background-color
border-radius: $notification-radius
padding: $notification-padding
position: relative
a:not(.button):not(.dropdown-item)
color: currentColor
text-decoration: underline
strong
color: currentColor
code,
pre
background: $white
pre code
background: transparent
& > .delete
position: absolute
right: 0.5rem
top: 0.5rem
.title,
.subtitle,
.content
color: currentColor
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
color: $color-invert

View file

@ -0,0 +1,39 @@
.block
@extend %block
.delete
@extend %delete
.heading
display: block
font-size: 11px
letter-spacing: 1px
margin-bottom: 5px
text-transform: uppercase
.highlight
@extend %block
font-weight: $weight-normal
max-width: 100%
overflow: hidden
padding: 0
pre
overflow: auto
max-width: 100%
.loader
@extend %loader
.number
align-items: center
background-color: $background
border-radius: $radius-rounded
display: inline-flex
font-size: $size-medium
height: 2em
justify-content: center
margin-right: 1.5rem
min-width: 2.5em
padding: 0.25rem 0.5rem
text-align: center
vertical-align: top

View file

@ -0,0 +1,65 @@
$progress-bar-background-color: $border !default
$progress-value-background-color: $text !default
$progress-indeterminate-duration: 1.5s !default
.progress
@extend %block
-moz-appearance: none
-webkit-appearance: none
border: none
border-radius: $radius-rounded
display: block
height: $size-normal
overflow: hidden
padding: 0
width: 100%
&::-webkit-progress-bar
background-color: $progress-bar-background-color
&::-webkit-progress-value
background-color: $progress-value-background-color
&::-moz-progress-bar
background-color: $progress-value-background-color
&::-ms-fill
background-color: $progress-value-background-color
border: none
&:indeterminate
animation-duration: $progress-indeterminate-duration
animation-iteration-count: infinite
animation-name: moveIndeterminate
animation-timing-function: linear
background-color: $progress-bar-background-color
background-image: linear-gradient(to right, $text 30%, $progress-bar-background-color 30%)
background-position: top left
background-repeat: no-repeat
background-size: 150% 150%
&::-webkit-progress-bar
background-color: transparent
&::-moz-progress-bar
background-color: transparent
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
&.is-#{$name}
&::-webkit-progress-value
background-color: $color
&::-moz-progress-bar
background-color: $color
&::-ms-fill
background-color: $color
&:indeterminate
background-image: linear-gradient(to right, $color 30%, $progress-bar-background-color 30%)
// Sizes
&.is-small
height: $size-small
&.is-medium
height: $size-medium
&.is-large
height: $size-large
@keyframes moveIndeterminate
from
background-position: 200% 0
to
background-position: -200% 0

View file

@ -0,0 +1,126 @@
$table-color: $grey-darker !default
$table-background-color: $white !default
$table-cell-border: 1px solid $grey-lighter !default
$table-cell-border-width: 0 0 1px !default
$table-cell-padding: 0.5em 0.75em !default
$table-cell-heading-color: $text-strong !default
$table-head-cell-border-width: 0 0 2px !default
$table-head-cell-color: $text-strong !default
$table-foot-cell-border-width: 2px 0 0 !default
$table-foot-cell-color: $text-strong !default
$table-head-background-color: transparent !default
$table-body-background-color: transparent !default
$table-foot-background-color: transparent !default
$table-row-hover-background-color: $white-bis !default
$table-row-active-background-color: $primary !default
$table-row-active-color: $primary-invert !default
$table-striped-row-even-background-color: $white-bis !default
$table-striped-row-even-hover-background-color: $white-ter !default
.table
@extend %block
background-color: $table-background-color
color: $table-color
td,
th
border: $table-cell-border
border-width: $table-cell-border-width
padding: $table-cell-padding
vertical-align: top
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
border-color: $color
color: $color-invert
// Modifiers
&.is-narrow
white-space: nowrap
width: 1%
&.is-selected
background-color: $table-row-active-background-color
color: $table-row-active-color
a,
strong
color: currentColor
th
color: $table-cell-heading-color
text-align: left
tr
&.is-selected
background-color: $table-row-active-background-color
color: $table-row-active-color
a,
strong
color: currentColor
td,
th
border-color: $table-row-active-color
color: currentColor
thead
background-color: $table-head-background-color
td,
th
border-width: $table-head-cell-border-width
color: $table-head-cell-color
tfoot
background-color: $table-foot-background-color
td,
th
border-width: $table-foot-cell-border-width
color: $table-foot-cell-color
tbody
background-color: $table-body-background-color
tr
&:last-child
td,
th
border-bottom-width: 0
// Modifiers
&.is-bordered
td,
th
border-width: 1px
tr
&:last-child
td,
th
border-bottom-width: 1px
&.is-fullwidth
width: 100%
&.is-hoverable
tbody
tr:not(.is-selected)
&:hover
background-color: $table-row-hover-background-color
&.is-striped
tbody
tr:not(.is-selected)
&:hover
background-color: $table-row-hover-background-color
&:nth-child(even)
background-color: $table-striped-row-even-hover-background-color
&.is-narrow
td,
th
padding: 0.25em 0.5em
&.is-striped
tbody
tr:not(.is-selected)
&:nth-child(even)
background-color: $table-striped-row-even-background-color
.table-container
@extend %block
+overflow-touch
overflow: auto
overflow-y: hidden
max-width: 100%

View file

@ -0,0 +1,130 @@
$tag-background-color: $background !default
$tag-color: $text !default
$tag-radius: $radius !default
$tag-delete-margin: 1px !default
.tags
align-items: center
display: flex
flex-wrap: wrap
justify-content: flex-start
.tag
margin-bottom: 0.5rem
&:not(:last-child)
margin-right: 0.5rem
&:last-child
margin-bottom: -0.5rem
&:not(:last-child)
margin-bottom: 1rem
// Sizes
&.are-medium
.tag:not(.is-normal):not(.is-large)
font-size: $size-normal
&.are-large
.tag:not(.is-normal):not(.is-medium)
font-size: $size-medium
&.has-addons
.tag
margin-right: 0
&:not(:first-child)
border-bottom-left-radius: 0
border-top-left-radius: 0
&:not(:last-child)
border-bottom-right-radius: 0
border-top-right-radius: 0
&.is-centered
justify-content: center
.tag
margin-right: 0.25rem
margin-left: 0.25rem
&.is-right
justify-content: flex-end
.tag
&:not(:first-child)
margin-left: 0.5rem
&:not(:last-child)
margin-right: 0
&.has-addons
.tag
margin-right: 0
&:not(:first-child)
margin-left: 0
border-bottom-left-radius: 0
border-top-left-radius: 0
&:not(:last-child)
border-bottom-right-radius: 0
border-top-right-radius: 0
.tag:not(body)
align-items: center
background-color: $tag-background-color
border-radius: $tag-radius
color: $tag-color
display: inline-flex
font-size: $size-small
height: 2em
justify-content: center
line-height: 1.5
padding-left: 0.75em
padding-right: 0.75em
white-space: nowrap
.delete
margin-left: 0.25rem
margin-right: -0.375rem
// Colors
@each $name, $pair in $colors
$color: nth($pair, 1)
$color-invert: nth($pair, 2)
&.is-#{$name}
background-color: $color
color: $color-invert
// Sizes
&.is-normal
font-size: $size-small
&.is-medium
font-size: $size-normal
&.is-large
font-size: $size-medium
.icon
&:first-child:not(:last-child)
margin-left: -0.375em
margin-right: 0.1875em
&:last-child:not(:first-child)
margin-left: 0.1875em
margin-right: -0.375em
&:first-child:last-child
margin-left: -0.375em
margin-right: -0.375em
// Modifiers
&.is-delete
margin-left: $tag-delete-margin
padding: 0
position: relative
width: 2em
&::before,
&::after
background-color: currentColor
content: ""
display: block
left: 50%
position: absolute
top: 50%
transform: translateX(-50%) translateY(-50%) rotate(45deg)
transform-origin: center center
&::before
height: 1px
width: 50%
&::after
height: 50%
width: 1px
&:hover,
&:focus
background-color: darken($tag-background-color, 5%)
&:active
background-color: darken($tag-background-color, 10%)
&.is-rounded
border-radius: $radius-rounded
a.tag
&:hover
text-decoration: underline

View file

@ -0,0 +1,64 @@
$title-color: $grey-darker !default
$title-size: $size-3 !default
$title-weight: $weight-semibold !default
$title-line-height: 1.125 !default
$title-strong-color: inherit !default
$title-strong-weight: inherit !default
$title-sub-size: 0.75em !default
$title-sup-size: 0.75em !default
$subtitle-color: $grey-dark !default
$subtitle-size: $size-5 !default
$subtitle-weight: $weight-normal !default
$subtitle-line-height: 1.25 !default
$subtitle-strong-color: $grey-darker !default
$subtitle-strong-weight: $weight-semibold !default
$subtitle-negative-margin: -1.25rem !default
.title,
.subtitle
@extend %block
word-break: break-word
em,
span
font-weight: inherit
sub
font-size: $title-sub-size
sup
font-size: $title-sup-size
.tag
vertical-align: middle
.title
color: $title-color
font-size: $title-size
font-weight: $title-weight
line-height: $title-line-height
strong
color: $title-strong-color
font-weight: $title-strong-weight
& + .highlight
margin-top: -0.75rem
&:not(.is-spaced) + .subtitle
margin-top: $subtitle-negative-margin
// Sizes
@each $size in $sizes
$i: index($sizes, $size)
&.is-#{$i}
font-size: $size
.subtitle
color: $subtitle-color
font-size: $subtitle-size
font-weight: $subtitle-weight
line-height: $subtitle-line-height
strong
color: $subtitle-strong-color
font-weight: $subtitle-strong-weight
&:not(.is-spaced) + .title
margin-top: $subtitle-negative-margin
// Sizes
@each $size in $sizes
$i: index($sizes, $size)
&.is-#{$i}
font-size: $size

View file

@ -0,0 +1,4 @@
@charset "utf-8"
@import "columns.sass"
@import "tiles.sass"

Some files were not shown because too many files have changed in this diff Show more