Merge pull request #2717 from coolo/remove_operator

Remove the origin operator and its user scripts
This commit is contained in:
Stephan Kulow 2022-02-22 10:03:31 +01:00 committed by GitHub
commit 31e0087399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1 additions and 1996 deletions

View File

@ -18,10 +18,7 @@ Apart from these tools, the repository includes:
located in the [dist](dist) directory.
* [GoCD](https://www.gocd.org) configuration files in [gocd](gocd). GoCD is an open source CI/CD
server that is used to deploy the bots on OBS.
* A set of [Tampermonkey](https://www.tampermonkey.net) scripts (see [userscript](userscript)
directory) to extend OBS features when using the web interface.
* Several [systemd](systemd) units: the Metrics and OBS Operator tools make use of
them.
* Several [systemd](systemd) units: the Metrics instance makes use of them.
## Tools
@ -332,13 +329,4 @@ changes to allow whitelisting before creating Bugzilla entries.
* Package: openSUSE-release-tools
* Usage: ???
#### obs-operator
Performs staging operations as a service instead of requiring the osc staging plugin to be utilized
directly.
* Sources: [obs_operator.py](obs_operator.py)
* Documentation: --
* Package: openSUSE-release-tools
* Usage: obsolete

View File

@ -187,18 +187,6 @@ Requires: perl-XML-Simple
Requires(pre): shadow
BuildArch: noarch
%package obs-operator
Summary: Server used to perform staging operations
Group: Development/Tools/Other
Requires: osc-plugin-origin = %{version}
Requires: osc-plugin-staging = %{version}
Requires(pre): shadow
BuildArch: noarch
%description obs-operator
Server used to perform staging operations as a service instead of requiring
the osc staging plugin to be utilized directly.
%description repo-checker
Repository checker service that inspects built RPMs from stagings.
@ -322,17 +310,6 @@ if [ -x %{_bindir}/systemctl ] && %{_bindir}/systemctl is-enabled grafana-server
%{_bindir}/systemctl try-restart --no-block grafana-server
fi
%pre obs-operator
getent passwd osrt-obs-operator > /dev/null || \
useradd -r -m -s /sbin/nologin -c "user for openSUSE-release-tools-obs-operator" osrt-obs-operator
exit 0
%postun obs-operator
%{systemd_postun}
if [ -x %{_bindir}/systemctl ] && %{_bindir}/systemctl is-enabled osrt-obs-operator ; then
%{_bindir}/systemctl try-restart --no-block osrt-obs-operator
fi
%pre origin-manager
getent passwd osrt-origin-manager > /dev/null || \
useradd -r -m -s /sbin/nologin -c "user for openSUSE-release-tools-origin-manager" osrt-origin-manager
@ -454,12 +431,6 @@ exit 0
%{_unitdir}/osrt-metrics-access.service
%{_unitdir}/osrt-metrics-access.timer
%files obs-operator
%{_bindir}/osrt-obs_operator
%{_unitdir}/osrt-obs-operator.service
%{_unitdir}/osrt-obs-operator-origin-manager-cron.service
%{_unitdir}/osrt-obs-operator-origin-manager-cron.timer
%files origin-manager
%{_bindir}/osrt-origin-manager
%{_datadir}/%{source_dir}/origin-manager.py

View File

@ -1,384 +0,0 @@
#!/usr/bin/python3 -u
# Without the -u option for unbuffered output nothing shows up in journal or
# kubernetes logs.
import argparse
import http
from http.cookies import SimpleCookie
from http.cookiejar import Cookie, LWPCookieJar
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
import json
import tempfile
import os
from osclib import common
import subprocess
import sys
import time
from urllib.parse import urlparse
from urllib.parse import parse_qs
# A cookie with an invalid key is intermittently generated on opensuse.org
# domain which causes the operator to crash when parsing the cookie. The desired
# cookie is valid, but cannot be utilize due to the exception. As suggested in
# https://stackoverflow.com/a/47012250, workaround by making EVERYTHING LEGAL!
http.cookies._is_legal_key = lambda _: True
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
def handle_error(self, request, client_address):
super().handle_error(request, client_address)
class RequestHandler(BaseHTTPRequestHandler):
COOKIE_NAME = 'openSUSE_session' # Both OBS and IBS.
GET_PATHS = [
'origin/config',
'origin/history',
'origin/list',
'origin/package',
'origin/potentials',
'origin/projects',
'origin/report',
'package/diff',
]
POST_PATHS = [
'request/submit',
'staging/select',
]
def do_OPTIONS(self):
try:
with OSCRequestEnvironment(self, require_session=False):
self.send_header('Access-Control-Allow-Methods', 'GET, POST')
self.send_header('Access-Control-Allow-Headers',
'Access-Control-Allow-Origin, Content-Type, X-Requested-With')
except OSCRequestEnvironmentException:
self.send_header('Allow', 'OPTIONS, GET, POST')
self.end_headers()
def do_GET(self):
url_parts = urlparse(self.path)
path = url_parts.path.lstrip('/')
path_parts = path.split('/')
path_prefix = '/'.join(path_parts[:2])
query = parse_qs(url_parts.query)
if path_prefix == '':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.write_string('namespace: {}\n'.format(common.NAME))
self.write_string('name: {}\n'.format('OBS Operator'))
self.write_string('version: {}\n'.format(common.VERSION))
return
if len(path_parts) < 3 or path_prefix not in self.GET_PATHS:
self.send_response(404)
self.end_headers()
return
try:
with OSCRequestEnvironment(self) as oscrc_file:
func = getattr(self, 'handle_{}'.format(path_prefix.replace('/', '_')))
command = func(path_parts[2:], query)
self.end_headers()
if command and not self.execute(oscrc_file, command):
self.write_string('failed')
except OSCRequestEnvironmentException as e:
self.write_string(str(e))
def do_POST(self):
url_parts = urlparse(self.path)
path = url_parts.path.lstrip('/')
path_parts = path.split('/')
path_prefix = '/'.join(path_parts[:2])
query = parse_qs(url_parts.query)
if len(path_parts) < 2 or path_prefix not in self.POST_PATHS:
self.send_response(404)
self.end_headers()
return
data = self.data_parse()
user = data.get('user')
if self.debug:
print('data: {}'.format(data))
try:
with OSCRequestEnvironment(self, user) as oscrc_file:
func = getattr(self, 'handle_{}'.format(path_prefix.replace('/', '_')))
commands = func(path_parts[2:], query, data)
self.end_headers()
for command in commands:
self.write_string('$ {}\n'.format(' '.join(command)))
if not self.execute(oscrc_file, command):
self.write_string('failed')
break
except OSCRequestEnvironmentException as e:
self.write_string(str(e))
def data_parse(self):
if int(self.headers['Content-Length']) == 0:
return {}
data = self.rfile.read(int(self.headers['Content-Length']))
return json.loads(data.decode('utf-8'))
def apiurl_get(self):
if self.apiurl:
return self.apiurl
host = self.headers.get('Host')
if not host:
return None
# Strip port if present.
domain = host.split(':', 2)[0]
if '.' not in domain:
return None
# Remove first subdomain and replace with api subdomain.
domain_parent = '.'.join(domain.split('.')[-2:])
return 'https://api.{}'.format(domain_parent)
def origin_domain_get(self):
origin = self.headers.get('Origin')
if origin is not None:
# Strip port if present.
domain = urlparse(origin).netloc.split(':', 2)[0]
if '.' in domain:
return '.'.join(domain.split('.')[-2:])
return None
def session_get(self):
if self.session:
return self.session
else:
cookie = self.headers.get('Cookie')
if cookie:
cookie = SimpleCookie(cookie)
if self.COOKIE_NAME in cookie:
return cookie[self.COOKIE_NAME].value
return None
def oscrc_create(self, oscrc_file, apiurl, cookiejar_file, user):
oscrc_file.write('\n'.join([
'[general]',
'apiurl = {}'.format(apiurl),
'cookiejar = {}'.format(cookiejar_file.name),
'staging.color = 0',
'[{}]'.format(apiurl),
'user = {}'.format(user),
'pass = invalid',
'',
]).encode('utf-8'))
oscrc_file.flush()
# In order to avoid osc clearing the cookie file the modified time of
# the oscrc file must be set further into the past.
# if int(round(config_mtime)) > int(os.stat(cookie_file).st_mtime):
recent_past = time.time() - 3600
os.utime(oscrc_file.name, (recent_past, recent_past))
def cookiejar_create(self, cookiejar_file, session):
cookie_jar = LWPCookieJar(cookiejar_file.name)
cookie_jar.set_cookie(Cookie(0, self.COOKIE_NAME, session,
None, False,
'', False, True,
'/', True,
True,
None, None, None, None, {}))
cookie_jar.save()
cookiejar_file.flush()
def execute(self, oscrc_file, command):
env = os.environ
env['OSC_CONFIG'] = oscrc_file.name
# Would be preferrable to stream incremental output, but python http
# server does not seem to support this easily.
result = subprocess.run(command, env=env, stdout=self.wfile, stderr=self.wfile)
return result.returncode == 0
def write_string(self, string):
self.wfile.write(string.encode('utf-8'))
def command_format_add(self, command, query):
format = None
if self.headers.get('Accept'):
format = self.headers.get('Accept').split('/', 2)[1]
if format != 'json' and format != 'yaml':
format = None
if not format and 'format' in query:
format = query['format'][0]
if format:
command.append('--format')
command.append(format)
def handle_origin_config(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'config']
if 'origins-only' in query:
command.append('--origins-only')
return command
def handle_origin_history(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'history']
self.command_format_add(command, query)
if len(args) > 1:
command.append(args[1])
return command
def handle_origin_list(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'list']
if 'force-refresh' in query:
command.append('--force-refresh')
self.command_format_add(command, query)
return command
def handle_origin_package(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'package']
if 'debug' in query:
command.append('--debug')
if len(args) > 1:
command.append(args[1])
return command
def handle_origin_potentials(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'potentials']
self.command_format_add(command, query)
if len(args) > 1:
command.append(args[1])
return command
def handle_origin_projects(self, args, query):
command = ['osc', 'origin', 'projects']
self.command_format_add(command, query)
return command
def handle_origin_report(self, args, query):
command = ['osc', 'origin', '-p', args[0], 'report']
if 'force-refresh' in query:
command.append('--force-refresh')
return command
def handle_package_diff(self, args, query):
# source_project source_package target_project [target_package] [source_revision] [target_revision]
command = ['osc', 'rdiff', args[0], args[1], args[2]] # len(args) == 3
if len(args) >= 4:
command.append(args[3]) # target_package
if len(args) >= 5:
command.append('--revision')
command.append(':'.join(args[4:6]))
return command
def handle_request_submit(self, args, query, data):
command = ['osc', 'sr', args[0], args[1], args[2]]
command.append('-m')
if 'message' in query and query['message'][0]:
command.append(query['message'][0])
else:
command.append('created via operator')
command.append('--yes')
return [command]
def staging_command(self, project, subcommand):
return ['osc', 'staging', '-p', project, subcommand]
def handle_staging_select(self, args, query, data):
for staging, requests in data['selection'].items():
command = self.staging_command(data['project'], 'select')
if 'move' in data and data['move']:
command.append('--move')
command.append(staging)
command.extend(requests)
yield command
class OSCRequestEnvironment(object):
def __init__(self, handler, user=None, require_session=True):
self.handler = handler
self.user = user
self.require_session = require_session
def __enter__(self):
apiurl = self.handler.apiurl_get()
origin_domain = self.handler.origin_domain_get()
if not apiurl or (not self.handler.apiurl and origin_domain and not apiurl.endswith(origin_domain)):
self.handler.send_response(400)
self.handler.end_headers()
if not apiurl:
raise OSCRequestEnvironmentException('unable to determine apiurl')
else:
raise OSCRequestEnvironmentException('origin does not match host domain')
session = self.handler.session_get()
if self.require_session and not session:
self.handler.send_response(401)
self.handler.end_headers()
raise OSCRequestEnvironmentException('unable to determine session')
if self.handler.debug:
print('apiurl: {}'.format(apiurl))
print('session: {}'.format(session))
self.handler.send_response(200)
self.handler.send_header('Content-type', 'text/plain')
if origin_domain:
self.handler.send_header('Access-Control-Allow-Credentials', 'true')
self.handler.send_header('Access-Control-Allow-Origin', self.handler.headers.get('Origin'))
self.cookiejar_file = tempfile.NamedTemporaryFile()
self.oscrc_file = tempfile.NamedTemporaryFile()
self.cookiejar_file.__enter__()
self.oscrc_file.__enter__()
self.handler.oscrc_create(self.oscrc_file, apiurl, self.cookiejar_file, self.user)
self.handler.cookiejar_create(self.cookiejar_file, session)
return self.oscrc_file
def __exit__(self, exc_type, exc_val, exc_tb):
self.cookiejar_file.__exit__(exc_type, exc_val, exc_tb)
self.oscrc_file.__exit__(exc_type, exc_val, exc_tb)
class OSCRequestEnvironmentException(Exception):
pass
def main(args):
RequestHandler.apiurl = args.apiurl
RequestHandler.session = args.session
RequestHandler.debug = args.debug
with ThreadedHTTPServer((args.host, args.port), RequestHandler) as httpd:
print('listening on {}:{}'.format(args.host, args.port))
httpd.serve_forever()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OBS Operator server used to perform staging operations.')
parser.set_defaults(func=main)
parser.add_argument('--host', default='', help='host name to which to bind')
parser.add_argument('--port', type=int, default=8080, help='port number to which to bind')
parser.add_argument('-A', '--apiurl',
help='OBS instance API URL to use instead of basing from request origin')
parser.add_argument('--session',
help='session cookie value to use instead of any passed cookie')
parser.add_argument('-d', '--debug', action='store_true',
help='print debugging information')
args = parser.parse_args()
sys.exit(args.func(args))

View File

@ -1,11 +0,0 @@
[Unit]
Description=openSUSE Release Tools: OBS Operator origin-manager cron
[Service]
User=osrt-obs-operator
SyslogIdentifier=osrt-obs-operator-origin-manager
ExecStart=/usr/bin/osc origin cron
RuntimeMaxSec=48 hour
[Install]
WantedBy=multi-user.target

View File

@ -1,9 +0,0 @@
[Unit]
Description=openSUSE Release Tools: OBS Operator origin-manager cron
[Timer]
OnCalendar=Sun,Tue,Thu *-*-* 04:00:00
Unit=osrt-obs-operator-origin-manager-cron.service
[Install]
WantedBy=timers.target

View File

@ -1,10 +0,0 @@
[Unit]
Description=openSUSE Release Tools: OBS Operator
[Service]
User=osrt-obs-operator
ExecStart=/usr/bin/osrt-obs_operator
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@ -1,21 +0,0 @@
# User Scripts
The scripts may be installed in one's browser using the Tampermonkey extension to provide additional features using OBS via the web. After installing the extension simply click on the link for the desired script below to install it. Any scripts that provide an interface for making changes depend on the user being logged in to the OBS instance with a user with the appropriate permissions to complete the task.
- [Origin](https://github.com/openSUSE/openSUSE-release-tools/raw/master/userscript/origin.user.js)
Supplement OBS interface with origin information. When viewing a package on OBS (`/package/show/$PACKAGE`) the origin information will be added to the links in the top right.
- [Staging Move Drag-n-Drop](https://github.com/openSUSE/openSUSE-release-tools/raw/master/userscript/staging-move-drag-n-drop.user.js)
Provides a drag-n-drop interface for moving requests between stagings using the staging dashboard. The staging dashboard can be found by visiting `/project/staging_projects/$PROJECT` on the relevant OBS instance where `$PROJECT` is the target project for the stagings (ex. `openSUSE:Factory` or `SUSE:SLE-15-SP1:GA`).
Once on the staging dashboard the option to `enter move mode` will be available in the legend on the right side. Either click the yellow box or press _ctrl + m_ as indicated when hovering over the box. After entering _move mode_ individual requests can be dragged between stagings or groups selected and moved together. Groups may be selected by either clicking in an open area and dragging a box around the desired requests to select them and/or by hold _ctrl_ and clicking on requests to add or remove them from the selections.
Once all desired moves have been made the _Apply_ button in the bottom center of the window may be press to apply the changes to the staging.
Note that the staging lock is still in effect and thus the moves will fail if someone else has acquired the staging lock. Also note that after a failure or decision to not go through with moves there is currently no way to leave/reset move mode, but reloading the page will clear any changes made in move mode.
## Troubleshooting
Additional information after a failed operation is available in the browser console which may be accessed by _right-clicking_ on the page and selecting _Inspect_ or _Inspect Element_ and clicking the _Console_ tab.

View File

@ -1,141 +0,0 @@
// ==UserScript==
// @name OSRT Origin
// @namespace openSUSE/openSUSE-release-tools
// @version 0.2.0
// @description Supplement OBS interface with origin information.
// @author Jimmy Berry
// @match */package/show/*
// @match */request/show/*
// @require https://code.jquery.com/jquery-3.3.1.min.js
// @grant none
// ==/UserScript==
jQuery.noConflict();
(function()
{
var pathParts = window.location.pathname.split('/');
if (pathParts[1] == 'package') {
var project = pathParts[pathParts.length - 2];
var package = pathParts[pathParts.length - 1];
origin_load(document.querySelector('ul.clean_list, ul.list-unstyled'), project, package);
} else if (pathParts[1] == 'request') {
request_actions_handle();
}
})();
function request_actions_handle() {
// Select all action tabs and store to avoid modification exceptions.
var action_elements = document.evaluate(
'//div[@class="card mb-3"][2]/div/div[@class="tab-content"]/div', document);
var actions = [];
var action;
while (action = action_elements.iterateNext()) {
actions.push(action);
}
for (var i = 0; i < actions.length; i++) {
action = actions[i];
// Select the side column containing build results.
var column = document.evaluate(
'div[@class="row"][2]//div[@class="card" and div[@data-buildresult-url]]',
action, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
// Select the text represtation of action. All other sources are
// inconsistent and do not always have the right values depending on
// request type or state. Still suffers from shortening with ellipses.
var summary = document.evaluate(
'div[1]/div[1]',
action, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
var parts = $(summary).text().trim().split(' ');
var request_type = parts[0].toLowerCase();
parts = parts.splice(-3);
var project = parts[0];
var package = parts[2];
if (request_type == 'release') {
// Maintenance release requests special (strip target package incident suffix).
package = package.split('.').slice(0, -1).join('.');
}
var card = document.createElement('div');
card.classList.add('card');
var list = document.createElement('ul');
list.classList.add('list-unstyled');
card.appendChild(list);
column.insertBefore(card, column.childNodes[0]);
origin_load(list, project, package);
}
}
function origin_load(element, project, package) {
// Add placeholder to indicated loading.
var item = document.createElement('li');
item.innerHTML = '<i class="fas fa-spinner fa-spin text-info"></i> Origin: loading...';
element.appendChild(item);
var url = operator_url() + '/origin/package/' + project + '/' + package;
$.get({url: url, crossDomain: true, xhrFields: {withCredentials: true}, success: function(origin) {
if (origin.endsWith('failed')) {
if (origin.startsWith('OSRT:OriginConfig attribute missing')) {
item.innerHTML = '';
} else {
origin_load_fail(item);
}
} else {
var origin_project = origin.trim();
if (origin_project.endsWith('~')) {
origin_project = origin_project.slice(0, -1);
}
item.innerHTML = '<i class="fas fa-external-link-alt text-info"></i> Origin: ';
if (origin_project != 'None') {
item.innerHTML += '<a href="/package/show/' + origin_project + '/' +
package + '">' + origin + '</a>'
} else {
item.innerHTML += origin;
}
url = web_interface_url() + '/web/origin-manager/#' + project + '/' + package;
if (origin_project != 'None') {
url += '/' + origin_project;
}
item = document.createElement('li');
item.innerHTML = '<i class="fas fa-external-link-alt text-info"></i> ' +
'<a target="_blank" href="' + url + '">Origin Manager Interface</a>';
element.appendChild(item);
}
}})
.fail(function() {
origin_load_fail(item);
});
}
function origin_load_fail(item) {
item.innerHTML = '<i class="fas fa-bug text-warning"></i> Origin: failed to load';
}
function operator_url() {
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
var subdomain = domain_parent.endsWith('suse.de') ? 'tortuga' : 'operator';
return 'https://' + subdomain + '.' + domain_parent;
}
function web_interface_url() {
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
var subdomain, path;
if (domain_parent.endsWith('suse.de')) {
subdomain = 'jberry.io';
path = '/osrt-web';
} else {
subdomain = 'osrt';
path = '';
}
return 'http://' + subdomain + '.' + domain_parent + path;
}

View File

@ -1,732 +0,0 @@
// ==UserScript==
// @name OSRT Staging Move Drag-n-Drop
// @namespace openSUSE/openSUSE-release-tools
// @version 0.2.0
// @description Provide staging request moving interface on staging dashboard.
// @author Jimmy Berry
// @match */project/staging_projects/*
// @require https://code.jquery.com/jquery-3.3.1.min.js
// @require https://raw.githubusercontent.com/p34eu/selectables/master/selectables.js
// @grant none
// ==/UserScript==
// Uses a combination of two sources:
// - https://www.sitepoint.com/accessible-drag-drop/ (modified slightly)
// - https://github.com/p34eu/selectables (used directly with abuse of modifier key option)
(function()
{
// Exclude not usable browsers.
if (!document.querySelectorAll || !('draggable' in document.createElement('span'))) {
return;
}
// Ensure user is logged in.
if (!document.querySelector('#link-to-user-home')) {
return;
}
// Add explanation of trigger shortcut to legend box.
var explanation = document.createElement('div');
explanation.id = 'osrt-explanation';
explanation.innerText = 'enter move mode';
explanation.setAttribute('title', 'ctrl + m');
explanation.onclick = function() {
initMoveInterface();
this.onclick = null;
};
document.querySelector('#legends').appendChild(explanation);
window.onkeyup = function(e) {
if (e.keyCode == 77 && e.ctrlKey) {
initMoveInterface();
}
}
// Include CSS immediately for explanation.
$('head').append(`<style>
#osrt-explanation
{
padding: 10px;
background-color: #d9b200;
color: white;
cursor: pointer;
}
#osrt-explanation.osrt-active
{
cursor: default;
}
#osrt-summary
{
position: sticky;
z-index: 10000;
bottom: 0;
left: 0;
width: 100%%;
height: 2em;
padding-top: 0.7em;
text-align: center;
background-color: white;
}
#osrt-summary button
{
margin-left: 10px;
}
#osrt-summary progress
{
display: none;
}
#osrt-summary.osrt-progress progress
{
display: inline;
}
#osrt-summary.osrt-progress span,
#osrt-summary.osrt-progress button,
#osrt-summary.osrt-success button
{
display: none;
}
#osrt-summary.osrt-failed
{
background-color: red;
color: white;
}
#osrt-summary.osrt-failed span
{
display: inline;
}
#osrt-summary.osrt-failed progress
{
display: none;
}
#osrt-summary.osrt-success
{
background-color: green;
color: white;
}
/* drop target state */
[data-draggable="target"][aria-dropeffect="move"]
{
border-color: #68b;
}
[data-draggable="target"][aria-dropeffect="move"]:focus,
[data-draggable="target"][aria-dropeffect="move"].dragover
{
box-shadow:0 0 0 1px #fff, 0 0 0 3px #68b;
}
[data-draggable="item"]:focus
{
box-shadow: 0 0 0 2px #68b, inset 0 0 0 1px #ddd;
}
[data-draggable="item"][aria-grabbed="true"],
.color-legend .selected
{
background: #8ad;
color: #fff;
}
.color-legend .moved,
.osrt-moved
{
background: repeating-linear-gradient(
45deg,
#606dbc,
#606dbc 10px,
#465298 10px,
#465298 20px
);
}
</style>`);
})();
var initMoveInterface = function() {
// Update explanation text and add new legend entries.
function addLegend(type)
{
var listItem = document.createElement('li');
var span = document.createElement('span');
span.classList.add(type.toLowerCase());
listItem.appendChild(span);
listItem.appendChild(document.createTextNode(type));
document.querySelector('ul.color-legend').appendChild(listItem);
}
addLegend('Moved');
addLegend('Selected');
var explanation = document.querySelector('#osrt-explanation');
explanation.innerText = 'drag box around requests or ctrl/shift + click requests to select and drag a request to another staging.';
explanation.setAttribute('title', 'move mode activated');
explanation.classList.add('osrt-active');
// @resource will not work since served without proper MIME type.
$.get('https://raw.githubusercontent.com/p34eu/selectables/master/selectables.css', function(data, status) {
$('head').append('<style>' + data + '</style>');
});
// Mark the drag targets and draggable items.
// Preferable to use the tr element as target, but mouse events not handled properly.
// Avoid making expand/collapse links selectable and avoid forcing them to be expanded.
// The pointer-events changes only seem to work properly from script.
$('table.staging-dashboard td').attr('data-draggable', 'target');
$('table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse))').attr('data-draggable', 'item').css('pointer-events', 'all');
// Disable mouse events on the request links as that makes it nearly impossible to drag them.
$('table.staging-dashboard ul.packages-list li.request:not(:has(a.staging_expand, a.staging_collapse)) a').css('pointer-events', 'none');
// Configure selectables to play nice with drag-n-drop code.
new Selectables({
elements: 'ul.packages-list li[data-draggable="item"]',
zone: 'body',
start: function (e) {
e.osrtContinue = (e.target.getAttribute('data-draggable') != 'item' &&
e.target.tagName != 'A' &&
e.target.tagName != 'LABEL' &&
!e.target.id.startsWith('osrt-'));
},
// Abuse key option by setting the value in start callback whic is run
// first and the value determines if drag selection is started.
key: 'osrtContinue',
onSelect: function (e) {
addSelection(e);
},
onDeselect: function (e) {
removeSelection(e);
}
});
function getStaging(item)
{
var parent;
if (item.tagName == 'TD') {
parent = item.parentElement;
} else {
parent = item.parentElement.parentElement.parentElement;
}
if (item.parentElement.classList.contains('staging_collapsible')) {
// An additional layer since in hidden container.
parent = parent.parentElement;
}
return parent.querySelector('div.letter a').innerText;
}
var summary = {};
function updateSummary()
{
var summaryElement = document.querySelector('div#osrt-summary');
if (!summaryElement) {
summaryElement = document.createElement('div');
summaryElement.id = 'osrt-summary';
summaryElement.appendChild(document.createElement('span'))
var button = document.createElement('button');
button.innerText = 'Apply';
button.onclick = applyChanges;
summaryElement.appendChild(button);
summaryElement.appendChild(document.createElement('progress'))
document.body.appendChild(summaryElement);
}
var elements = document.querySelectorAll('.osrt-moved');
summary = {};
var staging;
for (var i = 0; i < elements.length; i++) {
staging = getStaging(elements[i]);
if (!isNaN(staging)) {
staging = 'adi:' + staging;
}
if (!(staging in summary)) {
summary[staging] = [];
}
summary[staging].push(elements[i].children[0].innerText.trim());
}
summaryElement.children[0].innerText = elements.length + ' request(s) to move affecting ' + Object.keys(summary).length + ' stagings(s)';
summaryElement.children[2].setAttribute('max', elements.length);
}
function applyChanges()
{
var summaryElement = document.querySelector('div#osrt-summary');
summaryElement.classList.add('osrt-progress');
var user = document.querySelector('#link-to-user-home').innerText.trim();
var pathParts = window.location.pathname.split('/');
var project = pathParts[pathParts.length - 1];
var data = JSON.stringify({'user': user, 'project': project, 'move': true, 'selection': summary});
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
var subdomain = domain_parent.endsWith('suse.de') ? 'tortuga' : 'operator';
var url = 'https://' + subdomain + '.' + domain_parent + '/staging/select';
$.post({url: url, data: data, crossDomain: true, xhrFields: {withCredentials: true},
success: applyChangesSuccess}).fail(applyChangesFailed);
}
function applyChangesSuccess(data)
{
// Could provide link to this in UI.
console.log(data);
var summaryElement = document.querySelector('div#osrt-summary');
summaryElement.classList.add('osrt-complete');
if (data.trim().endsWith('failed')) {
applyChangesFailed();
return;
}
var expected = summaryElement.children[2].getAttribute('max');
if ((data.match(/\(\d+\/\d+\)/g) || []).length == expected) {
summaryElement.children[0].innerText = 'Moved ' + expected + ' request(s).';
summaryElement.children[2].setAttribute('value', expected);
summaryElement.classList.add('osrt-success');
summaryElement.classList.remove('osrt-progress');
// Could reset UI in a more elegant way.
reloadShortly();
return;
}
applyChangesFailed();
}
function applyChangesFailed()
{
var summaryElement = document.querySelector('div#osrt-summary');
summaryElement.children[0].innerText = 'Failed to move requests.';
summaryElement.classList.add('osrt-failed');
}
function reloadShortly()
{
setTimeout(function() { window.location.reload(); }, 3000);
}
//get the collection of draggable targets and add their draggable attribute
for(var
targets = document.querySelectorAll('[data-draggable="target"]'),
len = targets.length,
i = 0; i < len; i ++)
{
targets[i].setAttribute('aria-dropeffect', 'none');
}
//get the collection of draggable items and add their draggable attributes
for(var
items = document.querySelectorAll('[data-draggable="item"]'),
len = items.length,
i = 0; i < len; i ++)
{
items[i].setAttribute('draggable', 'true');
items[i].setAttribute('aria-grabbed', 'false');
items[i].setAttribute('tabindex', '0');
// OSRT modification: keep track of original staging.
items[i].setAttribute('data-staging-origin', getStaging(items[i]));
}
//dictionary for storing the selections data
//comprising an array of the currently selected items
//a reference to the selected items' owning container
//and a refernce to the current drop target container
var selections =
{
items : [],
owner : null,
droptarget : null
};
//function for selecting an item
function addSelection(item)
{
//if the owner reference is still null, set it to this item's parent
//so that further selection is only allowed within the same container
if(!selections.owner)
{
selections.owner = item.parentNode;
}
//or if that's already happened then compare it with this item's parent
//and if they're not the same container, return to prevent selection
else if(selections.owner != item.parentNode)
{
return;
}
//set this item's grabbed state
item.setAttribute('aria-grabbed', 'true');
//add it to the items array
selections.items.push(item);
}
//function for unselecting an item
function removeSelection(item)
{
//reset this item's grabbed state
item.setAttribute('aria-grabbed', 'false');
//then find and remove this item from the existing items array
for(var len = selections.items.length, i = 0; i < len; i ++)
{
if(selections.items[i] == item)
{
selections.items.splice(i, 1);
break;
}
}
}
//function for resetting all selections
function clearSelections()
{
//if we have any selected items
if(selections.items.length)
{
//reset the owner reference
selections.owner = null;
//reset the grabbed state on every selected item
for(var len = selections.items.length, i = 0; i < len; i ++)
{
selections.items[i].setAttribute('aria-grabbed', 'false');
}
//then reset the items array
selections.items = [];
}
}
//shorctut function for testing whether a selection modifier is pressed
function hasModifier(e)
{
return (e.ctrlKey || e.metaKey || e.shiftKey);
}
//function for applying dropeffect to the target containers
function addDropeffects()
{
//apply aria-dropeffect and tabindex to all targets apart from the owner
for(var len = targets.length, i = 0; i < len; i ++)
{
if
(
targets[i] != selections.owner
&&
targets[i].getAttribute('aria-dropeffect') == 'none'
)
{
targets[i].setAttribute('aria-dropeffect', 'move');
targets[i].setAttribute('tabindex', '0');
}
}
//remove aria-grabbed and tabindex from all items inside those containers
for(var len = items.length, i = 0; i < len; i ++)
{
if
(
items[i].parentNode != selections.owner
&&
items[i].getAttribute('aria-grabbed')
)
{
items[i].removeAttribute('aria-grabbed');
items[i].removeAttribute('tabindex');
}
}
}
//function for removing dropeffect from the target containers
function clearDropeffects()
{
//if we have any selected items
if(selections.items.length)
{
//reset aria-dropeffect and remove tabindex from all targets
for(var len = targets.length, i = 0; i < len; i ++)
{
if(targets[i].getAttribute('aria-dropeffect') != 'none')
{
targets[i].setAttribute('aria-dropeffect', 'none');
targets[i].removeAttribute('tabindex');
}
}
//restore aria-grabbed and tabindex to all selectable items
//without changing the grabbed value of any existing selected items
for(var len = items.length, i = 0; i < len; i ++)
{
if(!items[i].getAttribute('aria-grabbed'))
{
items[i].setAttribute('aria-grabbed', 'false');
items[i].setAttribute('tabindex', '0');
}
else if(items[i].getAttribute('aria-grabbed') == 'true')
{
items[i].setAttribute('tabindex', '0');
}
}
}
}
//shortcut function for identifying an event element's target container
function getContainer(element)
{
do
{
if(element.nodeType == 1 && element.getAttribute('aria-dropeffect'))
{
return element;
}
}
while(element = element.parentNode);
return null;
}
//mousedown event to implement single selection
document.addEventListener('mousedown', function(e)
{
//if the element is a draggable item
if(e.target.getAttribute('draggable'))
{
//clear dropeffect from the target containers
clearDropeffects();
//if the multiple selection modifier is not pressed
//and the item's grabbed state is currently false
if
(
!hasModifier(e)
&&
e.target.getAttribute('aria-grabbed') == 'false'
)
{
//clear all existing selections
clearSelections();
//then add this new selection
addSelection(e.target);
}
}
//else [if the element is anything else]
//and the selection modifier is not pressed
else if(!hasModifier(e))
{
//clear dropeffect from the target containers
clearDropeffects();
//clear all existing selections
clearSelections();
}
//else [if the element is anything else and the modifier is pressed]
else
{
//clear dropeffect from the target containers
clearDropeffects();
}
}, false);
//mouseup event to implement multiple selection
document.addEventListener('mouseup', function(e)
{
//if the element is a draggable item
//and the multipler selection modifier is pressed
if(e.target.getAttribute('draggable') && hasModifier(e))
{
//if the item's grabbed state is currently true
if(e.target.getAttribute('aria-grabbed') == 'true')
{
//unselect this item
removeSelection(e.target);
//if that was the only selected item
//then reset the owner container reference
if(!selections.items.length)
{
selections.owner = null;
}
}
//else [if the item's grabbed state is false]
else
{
//add this additional selection
addSelection(e.target);
}
}
}, false);
//dragstart event to initiate mouse dragging
document.addEventListener('dragstart', function(e)
{
//if the element's parent is not the owner, then block this event
if(selections.owner != e.target.parentNode)
{
e.preventDefault();
return;
}
//[else] if the multiple selection modifier is pressed
//and the item's grabbed state is currently false
if
(
hasModifier(e)
&&
e.target.getAttribute('aria-grabbed') == 'false'
)
{
//add this additional selection
addSelection(e.target);
}
//we don't need the transfer data, but we have to define something
//otherwise the drop action won't work at all in firefox
//most browsers support the proper mime-type syntax, eg. "text/plain"
//but we have to use this incorrect syntax for the benefit of IE10+
e.dataTransfer.setData('text', '');
//apply dropeffect to the target containers
addDropeffects();
}, false);
//related variable is needed to maintain a reference to the
//dragleave's relatedTarget, since it doesn't have e.relatedTarget
var related = null;
//dragenter event to set that variable
document.addEventListener('dragenter', function(e)
{
related = e.target;
}, false);
//dragleave event to maintain target highlighting using that variable
document.addEventListener('dragleave', function(e)
{
//get a drop target reference from the relatedTarget
var droptarget = getContainer(related);
//if the target is the owner then it's not a valid drop target
if(droptarget == selections.owner)
{
droptarget = null;
}
//if the drop target is different from the last stored reference
//(or we have one of those references but not the other one)
if(droptarget != selections.droptarget)
{
//if we have a saved reference, clear its existing dragover class
if(selections.droptarget)
{
selections.droptarget.className =
selections.droptarget.className.replace(/ dragover/g, '');
}
//apply the dragover class to the new drop target reference
if(droptarget)
{
droptarget.className += ' dragover';
}
//then save that reference for next time
selections.droptarget = droptarget;
}
}, false);
//dragover event to allow the drag by preventing its default
document.addEventListener('dragover', function(e)
{
//if we have any selected items, allow them to be dragged
if(selections.items.length)
{
e.preventDefault();
}
}, false);
//dragend event to implement items being validly dropped into targets,
//or invalidly dropped elsewhere, and to clean-up the interface either way
document.addEventListener('dragend', function(e)
{
//if we have a valid drop target reference
//(which implies that we have some selected items)
if(selections.droptarget)
{
// OSRT modification: only move if location is changing.
if (getStaging(selections.droptarget) == getStaging(selections.items[0])) {
e.preventDefault();
return;
}
// OSRT modification: place requests back in package list.
var target = selections.droptarget.parentElement.querySelector('ul.packages-list');
//append the selected items to the end of the target container
for(var len = selections.items.length, i = 0; i < len; i ++)
{
// OSRT modification: place in package list and determine if moved from origin.
// selections.droptarget.appendChild(selections.items[i]);
target.appendChild(selections.items[i]);
if (getStaging(selections.items[i]) != selections.items[i].getAttribute('data-staging-origin'))
{
selections.items[i].classList.add('osrt-moved');
}
else {
selections.items[i].classList.remove('osrt-moved');
}
}
// OSRT modification: after drag update overall summary of moves.
updateSummary();
//prevent default to allow the action
e.preventDefault();
}
//if we have any selected items
if(selections.items.length)
{
//clear dropeffect from the target containers
clearDropeffects();
//if we have a valid drop target reference
if(selections.droptarget)
{
//reset the selections array
clearSelections();
//reset the target's dragover class
selections.droptarget.className =
selections.droptarget.className.replace(/ dragover/g, '');
//reset the target reference
selections.droptarget = null;
}
}
}, false);
};

View File

@ -1,41 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.2.3/css/bootstrap/tabulator_bootstrap.min.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/diff2html/2.7.0/diff2html.css">
<link rel="stylesheet" href="main.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.2.3/js/tabulator.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/diff2html/2.7.0/diff2html.js"></script>
<script src="util.js"></script>
<script src="main.js"></script>
<link rel="shortcut icon" href="https://www.gravatar.com/avatar/590fa7a9bbeceb2e07c820ec3bec5e44?s=20&d=wavatar">
<title>Origin Manager</title>
</head>
<body>
<main>
<section>
<div id="project-table"></div>
</section>
<aside>
<div id="potential-table"></div>
<div id="history-table"></div>
</aside>
<div id="details">
</div>
</main>
<script>
project_table_init('#project-table');
potential_table_init('#potential-table');
history_table_init('#history-table');
hash_init();
</script>
</body>
</html>

View File

@ -1,35 +0,0 @@
main {
clear: both;
}
section, aside, #details {
float: left;
}
section {
min-width: 700px;
height: 100%;
}
section #project-table {
height: inherit;
margin-bottom: 0;
}
aside {
min-width: 400px;
max-width: 600px;
display: none;
}
#details {
display: none;
min-width: 600px;
max-width: 800px;
height: 100%;
overflow: auto;
}
#details p, #details pre {
margin: 20px;
}

View File

@ -1,560 +0,0 @@
var OBS_URL = obs_url();
var FETCH_CONFIG = {
method: 'GET',
mode: 'cors',
credentials: 'include',
};
var FETCH_JSON_CONFIG = Object.assign({}, FETCH_CONFIG, {
headers: {
'Accept': 'application/json',
},
});
var POST_CONFIG = Object.assign({}, FETCH_CONFIG, {
method: 'POST',
});
function origin_project(origin) {
return origin.replace(/[+~]$/, '')
}
function param_lookup_package(cell) {
return {
urlPrefix: OBS_URL + '/package/show/' + project_get() + '/',
target: '_blank',
};
}
function param_lookup_origin(cell) {
var origin = cell.getValue();
if (origin == 'None') {
return { url: '#' };
}
var params = {
labelField: 'origin',
urlField: 'package',
urlPrefix: OBS_URL + '/package/show/' + origin_project(origin) + '/',
target: '_blank',
};
if (cell.getTable().package) {
params['url'] = params['urlPrefix'] + cell.getTable().package;
params['urlPrefix'] = null;
}
return params;
}
function param_lookup_request(cell) {
if (cell.getValue() == null) {
return { label: ' ', url: '#' }
}
return {
urlPrefix: OBS_URL + '/request/show/',
target: '_blank',
};
}
function formatter_tristate(cell, formatterParams, onRendered) {
onRendered(function() {
$(cell.getElement()).sparkline(cell.getValue(), {
type: 'tristate',
width: '100%',
barWidth: 14,
disableTooltips: true,
});
});
}
function sorter_tristate(a, b, aRow, bRow, column, dir, sorterParams) {
return sorter_tristate_distance(a) - sorter_tristate_distance(b);
}
function sorter_request(a, b, aRow, bRow, column, dir, sorterParams) {
if (a == b) return 0;
if (a == null) return (dir == 'asc' ? 1 : -1) * Number.MAX_SAFE_INTEGER;
if (b == null) return (dir == 'asc' ? -1 : 1) * Number.MAX_SAFE_INTEGER;
return a - b;
}
function sorter_tristate_distance(revisions) {
var distance = 0;
for (var i = revisions.length - 1; i >= 0; i--) {
if (revisions[i] === -1) distance += 10;
else if (revisions[i] === 0) distance += 1;
else break
}
return distance
}
function table_selection_get(table) {
if (table.getSelectedRows().length > 0) {
return table.getSelectedRows()[0].getIndex();
}
return null;
}
function table_selection_set(table, value) {
if (typeof table === 'undefined') return;
if (table.getSelectedRows().length > 0) {
if (table.getSelectedRows()[0].getIndex() != value) {
table.getSelectedRows()[0].deselect();
table.selectRow(value);
setTimeout(function(){ table.scrollToRow(value, 'middle', false) }, 500);
}
} else {
table.selectRow(value);
setTimeout(function(){ table.scrollToRow(value, 'middle', false) }, 500);
}
}
var project_table;
function project_table_init(selector) {
project_table = new Tabulator(selector, {
columns: [
{
title: 'Package',
field: 'package',
headerFilter: 'input',
width: 200,
formatter: 'link',
formatterParams: param_lookup_package,
},
{
title: 'Origin',
field: 'origin',
headerFilter: 'input',
width: 200,
formatter: 'link',
formatterParams: param_lookup_origin,
},
{
title: 'Revisions',
field: 'revisions',
width: 170,
formatter: formatter_tristate,
sorter: sorter_tristate,
},
{
title: 'Request',
field: 'request',
headerFilter: 'input',
width: 100,
formatter: 'link',
formatterParams: param_lookup_request,
sorter: sorter_request,
},
],
dataLoaded: package_select_set,
index: 'package',
initialSort: [
{ column: 'package', dir: 'asc' },
],
rowClick: package_select_hash,
selectable: 1,
tooltips: true,
});
}
var potential_table;
function potential_table_init(selector) {
potential_table = new Tabulator(selector, {
columns: [
{
title: 'Origin',
field: 'origin',
headerFilter: 'input',
width: 200,
formatter: 'link',
formatterParams: param_lookup_origin,
},
{
title: 'Version',
field: 'version',
headerFilter: 'input',
width: 100,
},
{
title: '',
formatter: function(cell, formatterParams, onRendered) {
return "<i class='fa fa-stream'></i>";
},
width: 20,
headerSort: false,
tooltip: 'diff',
cellClick: potential_external,
},
{
title: '',
formatter: function(cell, formatterParams, onRendered) {
return "<i class='fa fa-share-square'></i>";
},
width: 20,
headerSort: false,
tooltip: 'submit',
cellClick: potential_submit_prompt,
},
],
dataLoaded: potential_select_set,
index: 'origin',
initialSort: [
{ column: 'origin', dir: 'asc' },
],
rowClick: potential_select_hash,
selectable: 1,
tooltips: true,
});
}
var history_table;
function history_table_init(selector) {
history_table = new Tabulator(selector, {
columns: [
{
title: 'Origin',
field: 'origin',
headerFilter: 'input',
width: 200,
formatter: 'link',
formatterParams: param_lookup_origin,
},
{
title: 'Request',
field: 'request',
headerFilter: 'input',
width: 100,
formatter: 'link',
formatterParams: param_lookup_request,
sorter: sorter_request,
},
{
title: 'State',
field: 'state',
headerFilter: 'input',
width: 100,
},
],
dataLoaded: history_select_set,
index: 'request',
initialSort: [
{ column: 'request', dir: 'desc' },
],
pagination: 'local',
paginationSize: 15,
rowClick: history_select_hash,
selectable: 1,
tooltips: true,
});
}
function project_prompt() {
const request = new Request(operator_url() + '/origin/projects/all', FETCH_JSON_CONFIG);
fetch(request)
.then(response => response.json())
.then(projects => {
var options = [];
for (var i = 0; i < projects.length; i++) {
options[i] = {text: projects[i], value: projects[i]};
}
bootbox.prompt({
title: 'Project',
inputType: 'select',
inputOptions: options,
closeButton: false,
callback: function(project) {
if (project) {
hash_set([project]);
}
}
});
});
}
function project_get() {
if ('project' in project_table) {
return project_table.project;
}
return null;
}
function project_set(project) {
if (project == project_get()) {
return;
}
project_table.project = project;
project_table.clearData();
project_table.setData(operator_url() + '/origin/list/' + project, {}, FETCH_JSON_CONFIG);
project_table.setHeaderFilterFocus('package');
}
function package_get() {
if (typeof potential_table !== 'undefined' && 'package' in potential_table) {
return potential_table.package;
}
return null;
}
function package_set(project, package) {
if (package == package_get()) {
return;
}
$('aside').toggle(package != null);
potential_table.package = package;
history_table.package = package;
if (package == null) return;
potential_table.clearData();
potential_table.setData(
operator_url() + '/origin/potentials/' + project + '/' + package, {}, FETCH_JSON_CONFIG);
history_table.clearData();
history_table.setData(
operator_url() + '/origin/history/' + project + '/' + package, {}, FETCH_JSON_CONFIG);
package_select_set();
}
function package_select_set() {
var package = package_get();
if (package) {
table_selection_set(project_table, package);
}
}
function package_select_hash() {
if (table_selection_get(project_table)) {
hash_set([project_get(), table_selection_get(project_table),
origin_project(project_table.getSelectedRows()[0].getData()['origin'])]);
} else {
hash_set([project_get()]);
}
}
function potential_get() {
return hash_get(2);
}
function potential_set(project, package, origin, request) {
if (project == $('#details').data('project') &&
package == $('#details').data('package') &&
origin == $('#details').data('origin') &&
request == $('#details').data('request')) return;
var path;
if (request != null) {
// Unlike a diff between origin and target project a diff between
// a request and an origin requires the request source project and
// package which are provided along with the history data. As such the
// history table must be loaded before the diff can be requested.
if (!history_table.getSelectedRows().length) return;
var request_data = history_table.getSelectedRows()[0].getData();
path = [
origin,
package,
request_data['source_project'],
request_data['source_package'],
'latest',
request_data['source_revision'],
].join('/');
} else {
path = [project, package, origin].join('/');
}
$('#details').toggle(origin != null);
$('#details').data('project', project);
$('#details').data('package', package);
$('#details').data('origin', origin);
$('#details').data('request', request);
// At minimum an origin is required, but be sure to toggle element and set
// data attributes to handle the next call properly.
if (origin == null) return;
$('#details').html('<p>Loading...</p>');
fetch(operator_url() + '/package/diff/' + path, FETCH_CONFIG)
.then(response => response.text())
.then(text => {
if (text == '') {
$('#details').html('<p>No difference</p>');
return;
} else if (text.startsWith('# diff failed')) {
$('#details').html('<p>Failed to generate diff</p><pre></pre>');
$('#details pre').text(text);
return;
}
$('#details').html(Diff2Html.getPrettyHtml(text));
});
potential_select_set();
}
function potential_select_set() {
var origin = potential_get();
if (origin) {
table_selection_set(potential_table, origin);
}
}
function potential_select_hash() {
hash_set([project_get(), package_get(), table_selection_get(potential_table),
table_selection_get(history_table)]);
}
function potential_external(e, cell) {
window.open(OBS_URL + '/package/rdiff/' + cell.getData()['origin'] + '/' + package_get() +
'?oproject=' + project_get(), '_blank');
}
function potential_submit_prompt(e, cell) {
bootbox.prompt({
title: 'Submit ' + cell.getData()['origin'] + '/' + package_get() + ' to ' + project_get() + '?',
size: 'large',
inputType: 'textarea',
closeButton: false,
callback: function(message) {
if (message === null) return;
potential_submit(project_get(), package_get(), cell.getData()['origin'], message);
}
});
}
function potential_submit(project, package, origin, message) {
fetch(operator_url() + '/request/submit/' + origin + '/' + package + '/' + project +
'?message=' + encodeURIComponent(message), POST_CONFIG)
.then(response => response.text())
.then(log => {
log = log.trim()
console.log(log);
var words = log.split(/\s+/);
var request = words[words.length - 1];
if (request == 'failed') {
bootbox.alert({
title: 'Failed to submit ' + origin + '/' + package + ' to ' + project,
message: '<pre>' + log.substr(0, log.length - 7) + '</pre>',
size: 'large',
closeButton: false,
});
return;
}
project_table.updateData([{'package': package, 'request': request}]);
});
}
function history_get() {
return hash_get(3);
}
function history_select_set() {
var request = history_get();
if (request) {
table_selection_set(history_table, request);
// Trigger appropriate potential_set() call since waiting for history
// data to be available for request diff.
hash_changed();
}
}
function history_select_hash() {
potential_select_hash();
}
var title_suffix;
function hash_init() {
title_suffix = document.title;
window.onhashchange = hash_changed;
hash_changed();
}
function hash_parts() {
return window.location.hash.substr(1).replace(/\/+$/, '').split('/');
}
function hash_get(index) {
var parts = hash_parts();
if (parts.length > index) {
return parts[index];
}
return null;
}
function hash_set(parts) {
// Shorten the parts array to before the first null.
for (var i = 0; i < parts.length; i++) {
if (parts[i] == null) {
parts.length = i;
break
}
}
window.location.hash = parts.join('/');
}
function hash_changed() {
// Wait until all tables have been initialized before proceeding.
if (typeof history_table === 'undefined') return;
var parts = hash_parts();
var project = null;
var package = null;
var origin = null;
var request = null;
// route: /*
if (parts[0] == '') {
project_prompt();
return;
}
// route: /:project
project = parts[0];
project_set(project);
// route: /:project/:package
if (parts.length >= 2) {
package = parts[1];
}
package_set(project, package);
// route: /:project/:package/:origin
if (parts.length >= 3) {
origin = parts[2];
}
// route: /:project/:package/:origin/:request
if (parts.length >= 4) {
request = parts[3];
}
potential_set(project, package, origin, request);
title_update(project, package, origin, request);
}
function title_update(project, package, origin, request) {
var parts = hash_parts();
var title = '';
if (project) {
title += project;
}
if (package) {
title += '/' + package;
}
if (request) {
title += ' request ' + request;
}
if (origin) {
title += ' diff against ' + origin;
}
if (title) {
document.title = title + ' - ' + title_suffix;
} else {
document.title = title_suffix;
}
}

View File

@ -1,10 +0,0 @@
function operator_url() {
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
var subdomain = domain_parent.endsWith('suse.de') ? 'tortuga' : 'operator';
return 'https://' + subdomain + '.' + domain_parent;
}
function obs_url() {
var domain_parent = window.location.hostname.split('.').splice(-2).join('.');
return 'https://build.' + domain_parent;
}