sovereign 0.14.2__py3-none-any.whl → 1.0.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- sovereign/__init__.py +17 -78
- sovereign/app.py +74 -59
- sovereign/cache/__init__.py +245 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +161 -0
- sovereign/cache/filesystem.py +74 -0
- sovereign/cache/types.py +17 -0
- sovereign/configuration.py +607 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +271 -100
- sovereign/dynamic_config/__init__.py +112 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/error_info.py +61 -0
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +2 -1
- sovereign/rendering.py +124 -0
- sovereign/rendering_common.py +91 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +123 -28
- sovereign/statistics.py +19 -21
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +9 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +103 -0
- sovereign/types.py +304 -0
- sovereign/utils/auth.py +27 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +3 -2
- sovereign/utils/eds.py +40 -22
- sovereign/utils/entry_point_loader.py +18 -0
- sovereign/utils/mock.py +60 -17
- sovereign/utils/resources.py +17 -0
- sovereign/utils/templates.py +4 -2
- sovereign/utils/timer.py +5 -3
- sovereign/utils/version_info.py +8 -0
- sovereign/utils/weighted_clusters.py +2 -1
- sovereign/v2/__init__.py +0 -0
- sovereign/v2/data/data_store.py +621 -0
- sovereign/v2/data/render_discovery_response.py +24 -0
- sovereign/v2/data/repositories.py +90 -0
- sovereign/v2/data/utils.py +33 -0
- sovereign/v2/data/worker_queue.py +273 -0
- sovereign/v2/jobs/refresh_context.py +117 -0
- sovereign/v2/jobs/render_discovery_job.py +145 -0
- sovereign/v2/logging.py +81 -0
- sovereign/v2/types.py +41 -0
- sovereign/v2/web.py +101 -0
- sovereign/v2/worker.py +199 -0
- sovereign/views/__init__.py +7 -0
- sovereign/views/api.py +82 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +52 -67
- sovereign/views/healthchecks.py +107 -20
- sovereign/views/interface.py +173 -117
- sovereign/worker.py +193 -0
- {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +81 -73
- sovereign-1.0.0a4.dist-info/RECORD +85 -0
- {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
- sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/panel.js +76 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -715
- sovereign/sources/__init__.py +0 -3
- sovereign/sources/file.py +0 -21
- sovereign/sources/inline.py +0 -38
- sovereign/sources/lib.py +0 -40
- sovereign/sources/poller.py +0 -298
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -64
- sovereign/views/admin.py +0 -120
- sovereign-0.14.2.dist-info/LICENSE.txt +0 -13
- sovereign-0.14.2.dist-info/RECORD +0 -45
- sovereign-0.14.2.dist-info/entry_points.txt +0 -10
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from starlette.exceptions import HTTPException
|
|
6
|
+
from yaml.parser import ParserError
|
|
7
|
+
from yaml.scanner import ScannerError
|
|
8
|
+
|
|
9
|
+
from sovereign import config, logs
|
|
10
|
+
|
|
11
|
+
type_urls = {
|
|
12
|
+
"v2": {
|
|
13
|
+
"listeners": "type.googleapis.com/envoy.api.v2.Listener",
|
|
14
|
+
"clusters": "type.googleapis.com/envoy.api.v2.Cluster",
|
|
15
|
+
"endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
|
16
|
+
"secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
|
|
17
|
+
"routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
|
|
18
|
+
"scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
|
|
19
|
+
},
|
|
20
|
+
"v3": {
|
|
21
|
+
"listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
|
|
22
|
+
"clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
|
|
23
|
+
"endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
|
|
24
|
+
"secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
|
|
25
|
+
"routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
|
|
26
|
+
"scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
|
|
27
|
+
"runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_type_urls(api_version, resource_type, resources):
|
|
33
|
+
type_url = type_urls.get(api_version, {}).get(resource_type)
|
|
34
|
+
if type_url is not None:
|
|
35
|
+
for resource in resources:
|
|
36
|
+
if not resource.get("@type"):
|
|
37
|
+
resource["@type"] = type_url
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def deserialize_config(content: str) -> dict[str, Any]:
|
|
41
|
+
try:
|
|
42
|
+
envoy_configuration = yaml.safe_load(content)
|
|
43
|
+
except (ParserError, ScannerError) as e:
|
|
44
|
+
logs.access_logger.queue_log_fields(
|
|
45
|
+
error=repr(e),
|
|
46
|
+
YAML_CONTEXT=e.context,
|
|
47
|
+
YAML_CONTEXT_MARK=e.context_mark,
|
|
48
|
+
YAML_NOTE=e.note,
|
|
49
|
+
YAML_PROBLEM=e.problem,
|
|
50
|
+
YAML_PROBLEM_MARK=e.problem_mark,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if config.sentry_dsn:
|
|
54
|
+
mod = importlib.import_module("sentry_sdk")
|
|
55
|
+
mod.capture_exception(e)
|
|
56
|
+
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=500,
|
|
59
|
+
detail=(
|
|
60
|
+
"Failed to load configuration, there may be "
|
|
61
|
+
"a syntax error in the configured templates. "
|
|
62
|
+
"Please check Sentry if you have configured Sentry DSN"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
if not isinstance(envoy_configuration, dict):
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Deserialized configuration is of unexpected format: {envoy_configuration}"
|
|
68
|
+
)
|
|
69
|
+
return envoy_configuration
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def filter_resources(
|
|
73
|
+
generated: list[dict[str, Any]], requested: list[str]
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
If Envoy specifically requested a resource, this removes everything
|
|
77
|
+
that does not match the name of the resource.
|
|
78
|
+
If Envoy did not specifically request anything, every resource is retained.
|
|
79
|
+
"""
|
|
80
|
+
if len(requested) == 0:
|
|
81
|
+
return generated
|
|
82
|
+
return [resource for resource in generated if resource_name(resource) in requested]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resource_name(resource: dict[str, Any]) -> str:
|
|
86
|
+
name = resource.get("name") or resource.get("cluster_name")
|
|
87
|
+
if isinstance(name, str):
|
|
88
|
+
return name
|
|
89
|
+
raise KeyError(
|
|
90
|
+
f"Failed to determine the name or cluster_name of the following resource: {resource}"
|
|
91
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from importlib.util import find_spec
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
json_response_class: Type[JSONResponse] = JSONResponse
|
|
7
|
+
if find_spec("orjson"):
|
|
8
|
+
from fastapi.responses import ORJSONResponse
|
|
9
|
+
|
|
10
|
+
json_response_class = ORJSONResponse
|
|
11
|
+
|
|
12
|
+
elif find_spec("ujson"):
|
|
13
|
+
from fastapi.responses import UJSONResponse
|
|
14
|
+
|
|
15
|
+
json_response_class = UJSONResponse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["json_response_class"]
|
sovereign/server.py
CHANGED
|
@@ -1,31 +1,126 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
import configparser
|
|
2
|
+
import tempfile
|
|
3
|
+
import warnings
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
from sovereign import application_logger as log
|
|
9
|
+
from sovereign.configuration import SovereignAsgiConfig, SupervisordConfig, config
|
|
10
|
+
from sovereign.v2.worker import Worker
|
|
11
|
+
|
|
12
|
+
# noinspection PyArgumentList
|
|
13
|
+
asgi_config = SovereignAsgiConfig()
|
|
14
|
+
# noinspection PyArgumentList
|
|
15
|
+
supervisord_config = SupervisordConfig()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def web(supervisor_enabled=True) -> None:
|
|
19
|
+
from sovereign.app import app
|
|
20
|
+
|
|
21
|
+
log.debug("Starting web server")
|
|
22
|
+
|
|
23
|
+
if not supervisor_enabled:
|
|
24
|
+
uvicorn.run(
|
|
25
|
+
app,
|
|
26
|
+
log_level=asgi_config.log_level,
|
|
27
|
+
access_log=False,
|
|
28
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
29
|
+
host=asgi_config.host,
|
|
30
|
+
port=asgi_config.port,
|
|
31
|
+
workers=1, # per managed supervisor proc
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
uvicorn.run(
|
|
35
|
+
app,
|
|
36
|
+
fd=0,
|
|
37
|
+
log_level=asgi_config.log_level,
|
|
38
|
+
access_log=False,
|
|
39
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
40
|
+
host=asgi_config.host,
|
|
41
|
+
port=asgi_config.port,
|
|
42
|
+
workers=1, # per managed supervisor proc
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def worker():
|
|
47
|
+
if config.worker_v2_enabled:
|
|
48
|
+
log.debug("Starting worker v2")
|
|
49
|
+
Worker().start()
|
|
50
|
+
else:
|
|
51
|
+
from sovereign.worker import worker as worker_app
|
|
52
|
+
|
|
53
|
+
log.debug("Starting worker")
|
|
54
|
+
uvicorn.run(
|
|
55
|
+
worker_app,
|
|
56
|
+
log_level=asgi_config.log_level,
|
|
57
|
+
access_log=False,
|
|
58
|
+
timeout_keep_alive=asgi_config.keepalive,
|
|
59
|
+
host="127.0.0.1",
|
|
60
|
+
port=9080,
|
|
61
|
+
workers=1, # per managed supervisor proc
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def write_supervisor_conf() -> Path:
|
|
66
|
+
proc_env = {
|
|
67
|
+
"LANG": "en_US.UTF-8",
|
|
68
|
+
"LC_ALL": "en_US.UTF-8",
|
|
69
|
+
}
|
|
70
|
+
base = {
|
|
71
|
+
"autostart": "true",
|
|
72
|
+
"autorestart": "true",
|
|
73
|
+
"stdout_logfile": "/dev/stdout",
|
|
74
|
+
"stdout_logfile_maxbytes": "0",
|
|
75
|
+
"stderr_logfile": "/dev/stderr",
|
|
76
|
+
"stderr_logfile_maxbytes": "0",
|
|
77
|
+
"stopsignal": "QUIT",
|
|
78
|
+
"environment": ",".join(["=".join((k, v)) for k, v in proc_env.items()]),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
conf = configparser.RawConfigParser()
|
|
82
|
+
conf["supervisord"] = supervisord = {
|
|
83
|
+
"nodaemon": str(supervisord_config.nodaemon).lower(),
|
|
84
|
+
"loglevel": supervisord_config.loglevel,
|
|
85
|
+
"pidfile": supervisord_config.pidfile,
|
|
86
|
+
"logfile": supervisord_config.logfile,
|
|
87
|
+
"directory": supervisord_config.directory,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
conf["fcgi-program:web"] = web = {
|
|
91
|
+
**base,
|
|
92
|
+
"socket": f"tcp://{asgi_config.host}:{asgi_config.port}",
|
|
93
|
+
"numprocs": str(asgi_config.workers),
|
|
94
|
+
"process_name": "%(program_name)s-%(process_num)02d",
|
|
95
|
+
"command": "sovereign-web", # default niceness, higher CPU priority
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
conf["program:data"] = worker = {
|
|
99
|
+
**base,
|
|
100
|
+
"numprocs": "1",
|
|
101
|
+
"command": "nice -n 2 sovereign-worker", # run worker with reduced CPU priority (higher niceness value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if user := asgi_config.user:
|
|
105
|
+
supervisord["user"] = user
|
|
106
|
+
web["user"] = user
|
|
107
|
+
worker["user"] = user
|
|
108
|
+
|
|
109
|
+
log.debug("Writing supervisor config")
|
|
110
|
+
with tempfile.NamedTemporaryFile("w", delete=False) as f:
|
|
111
|
+
conf.write(f)
|
|
112
|
+
log.debug("Supervisor config written out")
|
|
113
|
+
return Path(f.name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main():
|
|
117
|
+
path = write_supervisor_conf()
|
|
118
|
+
with warnings.catch_warnings():
|
|
119
|
+
warnings.simplefilter("ignore")
|
|
120
|
+
from supervisor import supervisord
|
|
121
|
+
|
|
122
|
+
log.debug("Starting processes")
|
|
123
|
+
supervisord.main(["-c", path])
|
|
29
124
|
|
|
30
125
|
|
|
31
126
|
if __name__ == "__main__":
|
sovereign/statistics.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional, Any, Callable, Dict
|
|
3
2
|
from functools import wraps
|
|
4
|
-
from
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
from sovereign.configuration import config as sovereign_config
|
|
6
|
+
|
|
7
|
+
STATSD: Dict[str, Optional["StatsDProxy"]] = {"instance": None}
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class StatsDProxy:
|
|
10
11
|
def __init__(self, statsd_instance: Optional[Any] = None) -> None:
|
|
11
12
|
self.statsd = statsd_instance
|
|
12
|
-
self.emitted = emitted
|
|
13
13
|
|
|
14
14
|
def __getattr__(self, item: str) -> Any:
|
|
15
15
|
if self.statsd is not None:
|
|
@@ -20,14 +20,12 @@ class StatsDProxy:
|
|
|
20
20
|
return self.do_nothing
|
|
21
21
|
|
|
22
22
|
def do_nothing(self, *args: Any, **kwargs: Any) -> None:
|
|
23
|
-
|
|
24
|
-
emitted[k] = emitted.setdefault(k, 0) + 1
|
|
23
|
+
_ = args[0]
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class StatsdNoop:
|
|
28
|
-
def __init__(self, *args
|
|
29
|
-
|
|
30
|
-
emitted[k] = emitted.setdefault(k, 0) + 1
|
|
27
|
+
def __init__(self, *args, **kwargs):
|
|
28
|
+
pass
|
|
31
29
|
|
|
32
30
|
def __enter__(self): # type: ignore
|
|
33
31
|
return self
|
|
@@ -43,21 +41,18 @@ class StatsdNoop:
|
|
|
43
41
|
return wrapped
|
|
44
42
|
|
|
45
43
|
|
|
46
|
-
def configure_statsd(
|
|
44
|
+
def configure_statsd() -> StatsDProxy:
|
|
45
|
+
if STATSD["instance"] is not None:
|
|
46
|
+
return STATSD["instance"]
|
|
47
|
+
config = sovereign_config.statsd
|
|
47
48
|
try:
|
|
48
49
|
from datadog import DogStatsd
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.emitted: Dict[str, Any] = dict()
|
|
54
|
-
self.emitted[metric] = self.emitted.setdefault(metric, 0) + 1
|
|
55
|
-
|
|
56
|
-
module: Optional[CustomStatsd]
|
|
57
|
-
module = CustomStatsd()
|
|
58
|
-
if config.enabled:
|
|
51
|
+
module: Optional[DogStatsd]
|
|
52
|
+
module = DogStatsd()
|
|
53
|
+
if config.enabled and module:
|
|
59
54
|
module.host = config.host
|
|
60
|
-
module.port = config.port
|
|
55
|
+
module.port = int(config.port)
|
|
61
56
|
module.namespace = config.namespace
|
|
62
57
|
module.use_ms = config.use_ms
|
|
63
58
|
for tag, value in config.tags.items():
|
|
@@ -71,4 +66,7 @@ def configure_statsd(config: StatsdConfig) -> StatsDProxy:
|
|
|
71
66
|
raise
|
|
72
67
|
module = None
|
|
73
68
|
|
|
74
|
-
|
|
69
|
+
ret = StatsDProxy(module)
|
|
70
|
+
if STATSD["instance"] is None:
|
|
71
|
+
STATSD["instance"] = ret
|
|
72
|
+
return ret
|
sovereign/templates/base.html
CHANGED
|
@@ -1,64 +1,77 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
7
7
|
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
|
8
8
|
<title>{% block title %}{% endblock %} - Sovereign</title>
|
|
9
|
+
<script src="/static/darkmode.js"></script>
|
|
9
10
|
{%- block head %}{% endblock %}
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
|
-
<div class="columns">
|
|
13
|
-
<div class="column"></div>
|
|
14
13
|
{%- block nav %}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<a
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
<div class="hero">
|
|
15
|
+
<div class="hero-body p-4">
|
|
16
|
+
<a href="#">
|
|
17
|
+
<h3 class="title is-3" style="margin-bottom: 0px">sovereign</h3>
|
|
18
|
+
<h7 class="title is-7">version {{ sovereign_version }}</h7>
|
|
19
|
+
</a>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="columns is-gapless" style="min-height: calc(100vh - 120px);">
|
|
24
|
+
<div class="column is-narrow" style="width: 250px;">
|
|
25
|
+
<aside class="menu p-4" style="height: 100%;">
|
|
26
|
+
<div class="mb-4">
|
|
27
|
+
<p class="menu-label">RESOURCES</p>
|
|
28
|
+
<ul class="menu-list">
|
|
29
|
+
{%- set active_page = resource_type|default('redirect_to_docs') -%}
|
|
30
|
+
{%- for type in all_types %}
|
|
31
|
+
<li style="padding: 2px">
|
|
32
|
+
<a href="/ui/resources/{{ type }}"
|
|
33
|
+
style="padding: 3px; border-radius: 4px"
|
|
34
|
+
{% if type == active_page %}class="is-active has-background-primary has-text-white"{% endif %}>
|
|
35
|
+
{{ type|capitalize }}
|
|
36
36
|
</a>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</
|
|
37
|
+
</li>
|
|
38
|
+
{%- endfor %}
|
|
39
|
+
</ul>
|
|
40
40
|
</div>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
|
|
42
|
+
<div class="mb-4">
|
|
43
|
+
<p class="menu-label">LINKS</p>
|
|
44
|
+
<ul class="menu-list">
|
|
45
|
+
<li>
|
|
46
|
+
<a href="/docs">OpenAPI Spec</a>
|
|
47
|
+
</li>
|
|
48
|
+
<li>
|
|
49
|
+
<a href="https://developer.atlassian.com/platform/sovereign/">Documentation</a>
|
|
50
|
+
</li>
|
|
51
|
+
<li>
|
|
52
|
+
<a href="https://bitbucket.org/atlassian/sovereign">Repository</a>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
47
55
|
</div>
|
|
56
|
+
|
|
57
|
+
<p class="menu-label">THEME</p>
|
|
58
|
+
<button id="dark-mode-toggle" class="button is-small">
|
|
59
|
+
<span>🌘</span>
|
|
60
|
+
</button>
|
|
61
|
+
</aside>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="column">
|
|
65
|
+
<div class="p-4" style="max-width: 1000px">
|
|
66
|
+
{%- endblock %}
|
|
67
|
+
{%- block body %}
|
|
68
|
+
{% endblock -%}
|
|
48
69
|
</div>
|
|
49
|
-
</
|
|
50
|
-
{%- endblock %}
|
|
51
|
-
|
|
52
|
-
{%- block subnav %}
|
|
53
|
-
{%- endblock %}
|
|
54
|
-
|
|
55
|
-
{%- block body %}
|
|
56
|
-
{% endblock -%}
|
|
70
|
+
</div>
|
|
57
71
|
</div>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</footer>
|
|
72
|
+
|
|
73
|
+
<footer class="footer">
|
|
74
|
+
{% block footer %}{% endblock %}
|
|
75
|
+
</footer>
|
|
63
76
|
</body>
|
|
64
77
|
</html>
|