{"action":"created","comment":{"url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/comments/70629664","id":70629664,"diff_hunk":"@@ -18,594 +18,104 @@\n  */\n \n require([\n-    \"jquery\",\n     \"base1/cockpit\",\n-    \"shell/po\",\n-    \"subscriptions/mustache\",\n-    \"subscriptions/patterns\",\n-], function($, cockpit, po, Mustache) {\n-    \"use strict\";\n-\n-    cockpit.locale(po);\n-    cockpit.translate();\n-    var _ = cockpit.gettext;\n-\n-    function debug() {\n-        if (window.debugging == \"all\" || window.debugging == \"subscriptions\")\n-            console.debug.apply(console, arguments);\n-    }\n-\n-    var subscriptions = { };\n-    var register_url = \"default\";\n-\n-    function register_system() {\n-        var url = null;\n-        var ex, errors = [];\n-        if (register_url != \"default\")\n-          url = $('#subscription-register-url-custom').val().trim();\n-        var username = $('#subscription-register-username').val();\n-        if (username === '') {\n-            ex = new Error(_(\"Login cannot be empty\"));\n-            ex.target = \"#subscription-register-username\";\n-            errors.push(ex);\n-        }\n-        var password = $('#subscription-register-password').val();\n-        if (password === '') {\n-            ex = new Error(_(\"Password cannot be empty\"));\n-            ex.target = \"#subscription-register-password\";\n-            errors.push(ex);\n-        }\n-        var org = $('#subscription-register-org').val();\n-\n-        $(\"#subscriptions-register-dialog\").dialog(\"failure\", errors);\n-\n-        if (!errors.length)\n-            subscriptions.manager.register_system(url, username, password, org);\n-    }\n-\n-    function register_dialog_initialize() {\n-        var dlg = $('#subscriptions-register-dialog');\n-        var btn = $('#account-register-start');\n-        btn.on('click', function() {\n-            register_system();\n-        });\n-\n-        dlg.dialog(\"failure\", null);\n-\n-        /* only clear password on show, everything else might be necessary again\n-         * if an earlier call to register the system failed\n-         */\n-        dlg.on('show.bs.modal', function() {\n-            $('#subscription-register-password').val(\"\");\n-            $('#subscriptions-register-password-note-details').hide();\n-        });\n-        dlg.on('shown.bs.modal', function() {\n-            $('#subscription-register-username').focus();\n-        });\n-\n-        dlg.on('keypress', function(e) {\n-            if (e.keyCode === 13)\n-              btn.trigger('click');\n-        });\n-    }\n-\n-    /*\n-     * gather and display information regarding system subscriptions\n-     *\n-     * update_info: if true, trigger an immediate update of the information\n-     */\n-    function subscription_manager(update_info, overall_status_change_callback) {\n-        var subscription_template = $(\"#subscription-template\").html();\n-        Mustache.parse(subscription_template);\n-\n-        var service = cockpit.dbus('com.redhat.SubscriptionManager');\n-\n-        var subscription_info = $('#subscriptions-subscribed');\n-        var system_unregistered = $('#subscriptions-unregistered');\n-        var is_updating = $('#subscriptions-updating');\n-\n-        var subcription_notification_success = $('#subscription-notification-success-template').html();\n-        Mustache.parse(subcription_notification_success);\n-\n-        /* keep track of calls to subscription-manager in progress so we don't call it concurrently\n-         * status_update_requested: refresh necessary after current action has finished\n-         */\n-        var status_update_requested = false;\n-\n-        var overall_status = null;\n-        var change_callback = overall_status_change_callback;\n-\n-        /* we trigger status update via dbus\n-         * if we don't get a timely reply, consider subscription-manager failure\n-         */\n-        var update_timeout;\n-\n-        /* The promise for the last operation */\n-        var last_promise;\n-\n-        function status_update_failed() {\n-            if (update_timeout !== null)\n-                $('#subscription-manager-not-found').show();\n-            update_timeout = null;\n-        }\n-\n-        function get_overall_status() {\n-            return overall_status;\n-        }\n-\n-        function render_subscriptions(info) {\n-            /* clear old subscription details */\n-            subscription_info.empty();\n-\n-            /* show subscription info only if there is visible information */\n-            if (info.length > 0) {\n-                subscription_info.show();\n-                /* render and add the new ones */\n-                for (var i = 0; i < info.length; ++i)\n-                    subscription_info.append( $(Mustache.render(subscription_template, info[i])) );\n+    \"subscriptions/react\",\n+    \"subscriptions/subscriptions-client\",\n+    \"subscriptions/subscriptions-register\",\n+    \"subscriptions/subscriptions-view\",\n+    \"subscriptions/cockpit-components-dialog\",\n+], function(cockpit, React, subscriptionsClient, subscriptionsRegister, subscriptionsView, Dialog) {\n+\n+\"use strict\";\n+\n+var _ = cockpit.gettext;\n+\n+var dataStore = { };\n+\n+function dataChanged() {\n+    dataStore.render();\n+}\n+\n+function dismissStatusError() {\n+    subscriptionsClient.subscriptionStatus.error = undefined;\n+    dataStore.render();\n+}\n+\n+var registerDialogDetails;\n+\n+function registerSystem () {\n+    return subscriptionsClient.registerSystem(registerDialogDetails);\n+}\n+\n+var footerProps = {\n+    'actions': [\n+          { 'clicked': registerSystem,\n+            'caption': _(\"Register\"),\n+            'style': 'primary',\n+          },\n+      ]\n+};\n+\n+function openRegisterDialog() {\n+    registerDialogDetails = subscriptionsRegister.defaultSettings();\n+\n+    // show dialog to register\n+    var renderDialog;\n+    var updatedData = function(prop, data) {\n+        if (prop) {\n+            if (data.target) {\n+                if (data.target.type == \"checkbox\") {\n+                    registerDialogDetails[prop] = data.target.checked;\n+                } else {\n+                    registerDialogDetails[prop] = data.target.value;\n+                    // input from the ui, so we don't need to re-render\n+                    return;\n+                }\n             } else {\n-                subscription_info.hide();\n+                registerDialogDetails[prop] = data;\n             }\n         }\n \n-        /*\n-         * Parses lines like:\n-         *\n-         * id:  text\n-         */\n-        function parse_single_subscription(text) {\n-            var ret = { };\n-            $.each(text.split('\\n'), function(i, line) {\n-                var pos = line.indexOf(':');\n-                if (pos !== -1)\n-                    ret[line.substring(0, pos)] = line.substring(pos + 1).trim();\n-            });\n-            return ret;\n-        }\n+        registerDialogDetails.onChange = updatedData;\n \n-        function parse_multiple_subscriptions(text) {\n-            var ret = [ ];\n-            var parts = text.split('\\n\\n');\n-            for (var i = 0; i < parts.length; ++i) {\n-                var segment = parts[i];\n-                if (segment.indexOf('Product Name:') === -1)\n-                    continue;\n-\n-                var segment_info = parse_single_subscription(segment);\n-\n-                var status = segment_info['Status'];\n-\n-                /* if we have status details, add those to the status */\n-                if (segment_info['Status Details'] !== '')\n-                    status = status + ' (' + segment_info['Status Details'] + ')';\n-\n-                /* convert text output to mustache template variables */\n-                ret.push({\n-                    'product_name': segment_info['Product Name'],\n-                    'product_id': segment_info['Product ID'],\n-                    'version': segment_info['Version'],\n-                    'arch': segment_info['Arch'],\n-                    'status': status,\n-                    'starts': segment_info['Starts'],\n-                    'ends': segment_info['Ends'],\n-                });\n-            }\n-            return ret;\n-        }\n-\n-        /*\n-         * Display information about an action in progress\n-         * Make sure we only have one subscription-manager instance at a time\n-         */\n-        var subscription_manager_running = false;\n-        function show_progress_message(message) {\n-            is_updating.show();\n-            $('#subscriptions-update-message').text(message);\n-        }\n-\n-        function hide_progress_message() {\n-            is_updating.hide();\n-        }\n-\n-        /* since we only call subscription_manager, we only need to check the update message visibility */\n-        function action_in_progress() {\n-            return (is_updating.is(':visible') || (last_promise && last_promise.state() == \"pending\"));\n-        }\n-\n-        /* get subscription details using 'subscription-manager list' */\n-        function get_subscription_details() {\n-            if (action_in_progress()) {\n-                status_update_requested = true;\n-                return;\n-            }\n-            status_update_requested = false;\n-\n-            /* we got a message from dbus, so subscription-manager seems to work */\n-            $('#subscription-manager-not-found').hide();\n-            $('#subscription-manager-not-accessible').hide();\n-\n-            show_progress_message('Retrieving current subscription details.');\n-\n-            /* TODO DBus API doesn't deliver what we need, so we call subscription manager\n-             * without translations and parse the output\n-             */\n-            cockpit.spawn(['subscription-manager', 'list'],\n-                    { directory: '/', superuser: \"try\", environ: ['LC_ALL=C'] })\n-                .done(function(output) {\n-                    hide_progress_message();\n-\n-                    var subscriptions = parse_multiple_subscriptions(output);\n-                    if ((subscriptions.length > 0)  && (output.indexOf('Subscribed') > -1))\n-                        render_subscriptions(subscriptions);\n-                    else\n-                        subscription_info.hide();\n-\n-                    /* if we have no subscriptions, tell user this */\n-                    if (subscription_info.children().length === 0)\n-                        $('#subscriptions-system-status').show();\n-                    else\n-                        $('#subscriptions-system-status').hide();\n-\n-                    /* if refresh was requested, try again - otherwise get details */\n-                    if (status_update_requested)\n-                        update_subscriptions();\n-\n-                })\n-                .fail(function(ex) {\n-                    hide_progress_message();\n-                    if (status_update_requested)\n-                        update_subscriptions();\n-                    /* calling subscription manager failed, display warning */\n-                    $('#subscription-manager-not-found').show();\n-                    console.warn(\"Subscriptions [get_subscription_details]: couldn't get details: \" + ex);\n-                });\n-        }\n-\n-        function remove_notifications() {\n-            $('div.container-fluid.alert').remove();\n-        }\n-\n-        function process_status_output(text, exit_details) {\n-            hide_progress_message();\n-\n-            if (exit_details.problem === 'access-denied') {\n-                $('#subscription-manager-not-found').hide();\n-                $('#subscription-manager-not-accessible').show();\n-                return;\n-            }\n-            /* if output isn't as expected, maybe not properly installed? */\n-            if (text.indexOf('Overall Status:') === -1) {\n-                console.log(text);\n-                $('#subscription-manager-not-found').show();\n-                $('#subscription-manager-not-accessible').hide();\n-                return;\n-            }\n-\n-            /* clear old subscription details */\n-            subscription_info.empty();\n-\n-            var status = parse_single_subscription(text);\n-            var current_system_status = status['Overall Status'];\n-\n-            if ((overall_status !== null) && (current_system_status !== overall_status)) {\n-                /* status changed, notify depending on actual change\n-                 * hide other message in case it's still visible\n-                 */\n-                remove_notifications();\n-                if (current_system_status === 'Current')\n-                    system_unregistered.parent().prepend( $(Mustache.render(subcription_notification_success)) );\n-\n-                if (typeof(change_callback) === typeof(Function))\n-                    change_callback(overall_status);\n-            }\n-            overall_status = current_system_status;\n-\n-            $('#subscriptions-system-status').hide();\n-\n-            /* only allow registration if system state is unknown\n-             * subscriptions might also be invalid (e.g. expired), but we don't handle that currently\n-             */\n-            if (overall_status !== 'Unknown') {\n-                system_unregistered.hide();\n-            }\n-            else {\n-                /* no valid subscription: hide output and present user with the option to register system */\n-                subscription_info.hide();\n-                system_unregistered.show();\n-            }\n-\n-            /* if refresh was requested, try again - otherwise get details */\n-            if (status_update_requested)\n-                update_subscriptions();\n-            else if (current_system_status !== 'Unknown')\n-                get_subscription_details();\n-        }\n-\n-        /* get subscription summary using 'subscription-manager status'*/\n-        function get_subscription_status() {\n-            if (action_in_progress()) {\n-                status_update_requested = true;\n-                return;\n-            }\n-            status_update_requested = false;\n-\n-            show_progress_message('Retrieving current system subscription status.');\n-\n-            /* we need a buffer for 'subscription-manager status' output, since that can fail with a return code != 0\n-             * even if we need its output (no valid subscription)\n-             */\n-            var status_buffer = '';\n-            /* TODO DBus API doesn't deliver what we need, so we call subscription manager\n-             * without translations and parse the output\n-             *\n-             * 'subscription-manager status' will only return with exit code 0 if all is well (and subscriptions current)\n-             */\n-            cockpit.spawn(['subscription-manager', 'status'],\n-                    { directory: '/', superuser: \"try\", environ: ['LC_ALL=C'], err: \"out\" })\n-                .stream(function(text) {\n-                    status_buffer += text;\n-                    return text.length;\n-                }).done(function(text) {\n-                    process_status_output(status_buffer + text, '');\n-                }).fail(function(ex) {\n-                    process_status_output(status_buffer, ex);\n-                });\n-        }\n-\n-        /* request update via DBus */\n-        function update_subscriptions() {\n-            status_update_requested = false;\n-            service.call(\n-                '/EntitlementStatus',\n-                'com.redhat.SubscriptionManager.EntitlementStatus',\n-                'check_status',\n-                []).catch(function(ex) {\n-                console.warn(\"EntitlementStatus.check_status() failed:\", ex);\n-            });\n-\n-            /* TODO: Don't use a timeout here. Needs better API */\n-            update_timeout = window.setTimeout(status_update_failed, 30000);\n-        }\n-\n-        function linkify_message(message) {\n-            /* we only consider there to be one link and return escaped html */\n-            var r = new RegExp(\"(https?:\\\\/\\\\/)[\\\\w_/?%:;@&=+$,\\\\\\\\\\\\-\\\\.!~*'#|]+\");\n-            var matches = r.exec(message);\n-            if (matches) {\n-                var idx = message.indexOf(matches[0]);\n-                var parts = {\n-                        'pre': message.substring(0, idx),\n-                        'link': matches[0],\n-                        'post': message.substring(idx+matches[0].length)\n-                    };\n-                return Mustache.render('{{pre}} <a target=\"_blank\" href=\"{{{link}}}\">{{link}}</a>{{post}}', parts);\n-            }\n-            return Mustache.render(\"{{text}}\", { 'text': message });\n-        }\n-\n-        function perform_register(url, username, password, org) {\n-            var deferred = $.Deferred();\n-\n-            var args = ['subscription-manager', 'register', '--auto-attach'];\n-            if (url !== null)\n-                args.push('--serverurl', url);\n-            /* if username or password are empty, subscription-manager will prompt for them\n-             * so we always need to pass them\n-             */\n-            args.push('--username', username, '--password', password);\n-\n-            // only pass org info if user provided it\n-            if (org)\n-                args.push('--org', org);\n-\n-            deferred.notify(_(\"Registering system...\"));\n-\n-            /* TODO DBus API doesn't deliver what we need, so we call subscription manager\n-             * without translations and parse the output\n-             */\n-            var process = cockpit.spawn(args, {\n-                directory: '/',\n-                superuser: \"require\",\n-                environ: ['LC_ALL=C'],\n-                err: \"out\"\n-            });\n-\n-            var promise;\n-            var buffer = '';\n-            process\n-                .input('')\n-                .always(function() {\n-                    if (last_promise === promise)\n-                        last_promise = null;\n-                })\n-                .stream(function(text) {\n-                    buffer += text;\n-                })\n-                .done(function(output) {\n-                    deferred.resolve();\n-                })\n-                .fail(function(ex) {\n-                    if (ex.problem === \"cancelled\") {\n-                        deferred.reject(ex);\n-                        return;\n-                    }\n-\n-                    /* detect error types we recognize, fall back is generic error */\n-                    var invalid_username_string = 'Invalid username or password.';\n-                    var invalid_credentials_string = 'Invalid Credentials';\n-                    var message;\n-                    var reason;\n-                    if (buffer.indexOf(invalid_username_string) === 0) {\n-                        reason = \"credentials\";\n-                        message = buffer.substring(invalid_username_string.length).trim();\n-                    } else if (buffer.indexOf(invalid_credentials_string) === 0) {\n-                        reason = \"credentials\";\n-                        message = buffer.substring(invalid_credentials_string.length).trim();\n-                    } else if (buffer.indexOf('Unable to reach the server at') === 0) {\n-                        reason = \"server\";\n-                        message = buffer.trim();\n-                    } else if ((buffer.indexOf('EOF') === 0) && (buffer.indexOf('Organization: ') !== -1)) {\n-                        reason = \"organization\";\n-                        message = _(\"Server expects an organization.\");\n-                    } else if (buffer.indexOf('The system has been registered') === 0) {\n-                        /*\n-                         * Currently we don't separate registration & subscription\n-                         * our auto-attach may have failed, but message tells us that\n-                         * registration was successful close the dialog and update status.\n-                         */\n-                        deferred.resolve();\n-                        return;\n-                    } else {\n-                        message = buffer.trim();\n-                    }\n-\n-                    if (!message) {\n-                        message = _(\"Failed to register the system\");\n-                        console.warn(ex.message);\n-                    }\n-                    var error = new Error(message);\n-                    error.reason = reason;\n-                    deferred.reject(error);\n-                });\n-\n-            promise = deferred.promise();\n-            promise.cancel = function cancel() {\n-                process.close(\"cancelled\");\n-            };\n-\n-            last_promise = promise;\n-            return promise;\n-        }\n-\n-        function register_system(url, username, password, org) {\n-            if (action_in_progress()) {\n-                console.log(_('Unable to register at this time because a call to subscription manager ' +\n-                  'is already in progress. Please try again.'));\n-                return;\n-            }\n-\n-            system_unregistered.hide();\n-            $('#subscriptions-register-password-note-details').hide();\n-            $('#subscriptions-register-org-note-details').hide();\n-\n-            var promise = perform_register(url, username, password, org);\n-            $(\"#subscriptions-register-dialog\").dialog(\"wait\", promise);\n-\n-            promise\n-                .done(function() {\n-                    $('#subscriptions-register-dialog').modal('hide');\n-                    /* trigger update just in case */\n-                    get_subscription_details();\n-                })\n-                .fail(function(ex) {\n-                    if (ex.problem == \"cancelled\")\n-                        return;\n-                    switch(ex.reason) {\n-                    case \"credentials\":\n-                        var ex1 = new Error(\"\");\n-                        ex1.target = \"#subscription-register-username\";\n-                        var ex2 = new Error(_(\"Invalid login or password.\"));\n-                        ex2.target = \"#subscription-register-password\";\n-                        $(\"#subscriptions-register-dialog\").dialog(\"failure\", ex1, ex2);\n-                        if (ex.message) {\n-                            $('#subscriptions-register-password-note-details')\n-                                .html(linkify_message(ex.message))\n-                                .show();\n-                        }\n-                        break;\n-                    case \"server\":\n-                        ex.target = \"#subscription-register-url\";\n-                        $(\"#subscriptions-register-dialog\").dialog(\"failure\", ex);\n-                        break;\n-                    case \"organization\":\n-                        ex.target = \"#subscription-register-org\";\n-                        $(\"#subscriptions-register-dialog\").dialog(\"failure\", ex);\n-                        break;\n-                    default:\n-                        $(\"#subscriptions-register-dialog\").dialog(\"failure\", ex);\n-                        break;\n-                    }\n-                    system_unregistered.show();\n-                });\n-        }\n-\n-        /* we want to get notified if subscription status of the system changes */\n-        service.subscribe(\n-            { path: '/EntitlementStatus',\n-              interface: 'com.redhat.SubscriptionManager.EntitlementStatus',\n-              member: 'entitlement_status_changed'\n-            },\n-            function(path, dbus_interface, signal, args) {\n-                update_timeout = null;\n-                /*\n-                 * status has changed, now get actual status via command line\n-                 * since older versions of subscription-manager don't deliver this via DBus\n-                 * note: subscription-manager needs superuser privileges\n-                 */\n-\n-                /* if we're already waiting for a result, don't call again */\n-                if (action_in_progress()) {\n-                    status_update_requested = true;\n-                    return;\n-                }\n-                get_subscription_status();\n-\n-            });\n-\n-        /* ideally we could get detailed subscription info via DBus, but we\n-         * can't rely on this being present on all systems we work on\n-         */\n-        /*service.subscribe(\n-            { path: \"/EntitlementStatus\",\n-              interface: \"org.freedesktop.DBUS.Properties\",\n-              member: \"PropertiesChanged\"\n-            }, function(path, interface, signal, args) {\n-            current.text('got properties');\n-        });*/\n-\n-        $('#subscriptions-register').on('click', function() {\n-            $('#subscriptions-register-dialog').modal('show');\n-        });\n-\n-        /* once we have a proper DBus API, we can trigger an update via DBus - update_subscriptions()\n-         * for now, read the relevant information directly to avoid delay\n-         */\n-        if (update_info) {\n-            get_subscription_status();\n-        }\n-\n-        return {\n-            'register_system':      register_system\n+        var dialogProps = {\n+            'title': _(\"Register system\"),\n+            'body': React.createElement(subscriptionsRegister.dialogBody, registerDialogDetails),\n         };\n-    }\n-\n-    register_dialog_initialize();\n-\n-    function register_url_changed() {\n-        var custom_url = $('#subscription-register-url-custom');\n-        if (register_url === \"default\") {\n-            custom_url.hide();\n-        } else {\n-            custom_url.show();\n-            custom_url.focus();\n-            custom_url.select();\n-        }\n-    }\n-\n-    function init(retrieve_current) {\n-        if (retrieve_current === undefined)\n-            retrieve_current = true;\n-\n-        $(\"#subscription-register-url\").on(\"click\", \"li\", function() {\n-            register_url = $(this).attr(\"value\");\n-            $(\"#subscription-register-url button span\").text($(this).text());\n-            register_url_changed();\n-        });\n-\n-        register_url = \"default\";\n-        register_url_changed();\n-        subscriptions.manager = subscription_manager(retrieve_current);\n-    }\n-\n-    init();\n+        if (renderDialog)\n+            renderDialog(dialogProps, footerProps);\n+        else\n+            renderDialog = Dialog.show_modal_dialog(dialogProps, footerProps);","path":"pkg/subscriptions/subscriptions.js","position":654,"original_position":654,"commit_id":"318962cdf012327af68dc9d0c1fec9cbeae7d3a7","original_commit_id":"318962cdf012327af68dc9d0c1fec9cbeae7d3a7","user":{"login":"dperpeet","id":8711649,"avatar_url":"https://avatars.githubusercontent.com/u/8711649?v=3","gravatar_id":"","url":"https://api.github.com/users/dperpeet","html_url":"https://github.com/dperpeet","followers_url":"https://api.github.com/users/dperpeet/followers","following_url":"https://api.github.com/users/dperpeet/following{/other_user}","gists_url":"https://api.github.com/users/dperpeet/gists{/gist_id}","starred_url":"https://api.github.com/users/dperpeet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dperpeet/subscriptions","organizations_url":"https://api.github.com/users/dperpeet/orgs","repos_url":"https://api.github.com/users/dperpeet/repos","events_url":"https://api.github.com/users/dperpeet/events{/privacy}","received_events_url":"https://api.github.com/users/dperpeet/received_events","type":"User","site_admin":false},"body":"> Why is this necessary here, but in other uses of the Dialog React class we don't need this?\r\n\r\nThe deciding factor here is where the decision to re-render comes from. If you change the state, React handles that internally. We pass in stuff via props though, so we need to manually trigger a new render. This is a convenience feature because the dialog pattern removes some \"complexity in the middle\".","created_at":"2016-07-13T13:58:58Z","updated_at":"2016-07-13T13:58:58Z","html_url":"https://github.com/cockpit-project/cockpit/pull/4715#discussion_r70629664","pull_request_url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715","_links":{"self":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/comments/70629664"},"html":{"href":"https://github.com/cockpit-project/cockpit/pull/4715#discussion_r70629664"},"pull_request":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715"}}},"pull_request":{"url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715","id":77099336,"html_url":"https://github.com/cockpit-project/cockpit/pull/4715","diff_url":"https://github.com/cockpit-project/cockpit/pull/4715.diff","patch_url":"https://github.com/cockpit-project/cockpit/pull/4715.patch","issue_url":"https://api.github.com/repos/cockpit-project/cockpit/issues/4715","number":4715,"state":"open","locked":false,"title":"Port subscriptions to React","user":{"login":"dperpeet","id":8711649,"avatar_url":"https://avatars.githubusercontent.com/u/8711649?v=3","gravatar_id":"","url":"https://api.github.com/users/dperpeet","html_url":"https://github.com/dperpeet","followers_url":"https://api.github.com/users/dperpeet/followers","following_url":"https://api.github.com/users/dperpeet/following{/other_user}","gists_url":"https://api.github.com/users/dperpeet/gists{/gist_id}","starred_url":"https://api.github.com/users/dperpeet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dperpeet/subscriptions","organizations_url":"https://api.github.com/users/dperpeet/orgs","repos_url":"https://api.github.com/users/dperpeet/repos","events_url":"https://api.github.com/users/dperpeet/events{/privacy}","received_events_url":"https://api.github.com/users/dperpeet/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-07-12T10:53:19Z","updated_at":"2016-07-13T13:58:58Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c79df3e091e67d32e2aa485b80b136c111ab7c5a","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715/commits","review_comments_url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715/comments","review_comment_url":"https://api.github.com/repos/cockpit-project/cockpit/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cockpit-project/cockpit/issues/4715/comments","statuses_url":"https://api.github.com/repos/cockpit-project/cockpit/statuses/318962cdf012327af68dc9d0c1fec9cbeae7d3a7","head":{"label":"dperpeet:subscription-keys","ref":"subscription-keys","sha":"318962cdf012327af68dc9d0c1fec9cbeae7d3a7","user":{"login":"dperpeet","id":8711649,"avatar_url":"https://avatars.githubusercontent.com/u/8711649?v=3","gravatar_id":"","url":"https://api.github.com/users/dperpeet","html_url":"https://github.com/dperpeet","followers_url":"https://api.github.com/users/dperpeet/followers","following_url":"https://api.github.com/users/dperpeet/following{/other_user}","gists_url":"https://api.github.com/users/dperpeet/gists{/gist_id}","starred_url":"https://api.github.com/users/dperpeet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dperpeet/subscriptions","organizations_url":"https://api.github.com/users/dperpeet/orgs","repos_url":"https://api.github.com/users/dperpeet/repos","events_url":"https://api.github.com/users/dperpeet/events{/privacy}","received_events_url":"https://api.github.com/users/dperpeet/received_events","type":"User","site_admin":false},"repo":{"id":23840026,"name":"cockpit","full_name":"dperpeet/cockpit","owner":{"login":"dperpeet","id":8711649,"avatar_url":"https://avatars.githubusercontent.com/u/8711649?v=3","gravatar_id":"","url":"https://api.github.com/users/dperpeet","html_url":"https://github.com/dperpeet","followers_url":"https://api.github.com/users/dperpeet/followers","following_url":"https://api.github.com/users/dperpeet/following{/other_user}","gists_url":"https://api.github.com/users/dperpeet/gists{/gist_id}","starred_url":"https://api.github.com/users/dperpeet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dperpeet/subscriptions","organizations_url":"https://api.github.com/users/dperpeet/orgs","repos_url":"https://api.github.com/users/dperpeet/repos","events_url":"https://api.github.com/users/dperpeet/events{/privacy}","received_events_url":"https://api.github.com/users/dperpeet/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/dperpeet/cockpit","description":"Cockpit, a user interface for GNU/Linux servers","fork":true,"url":"https://api.github.com/repos/dperpeet/cockpit","forks_url":"https://api.github.com/repos/dperpeet/cockpit/forks","keys_url":"https://api.github.com/repos/dperpeet/cockpit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dperpeet/cockpit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dperpeet/cockpit/teams","hooks_url":"https://api.github.com/repos/dperpeet/cockpit/hooks","issue_events_url":"https://api.github.com/repos/dperpeet/cockpit/issues/events{/number}","events_url":"https://api.github.com/repos/dperpeet/cockpit/events","assignees_url":"https://api.github.com/repos/dperpeet/cockpit/assignees{/user}","branches_url":"https://api.github.com/repos/dperpeet/cockpit/branches{/branch}","tags_url":"https://api.github.com/repos/dperpeet/cockpit/tags","blobs_url":"https://api.github.com/repos/dperpeet/cockpit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dperpeet/cockpit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dperpeet/cockpit/git/refs{/sha}","trees_url":"https://api.github.com/repos/dperpeet/cockpit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dperpeet/cockpit/statuses/{sha}","languages_url":"https://api.github.com/repos/dperpeet/cockpit/languages","stargazers_url":"https://api.github.com/repos/dperpeet/cockpit/stargazers","contributors_url":"https://api.github.com/repos/dperpeet/cockpit/contributors","subscribers_url":"https://api.github.com/repos/dperpeet/cockpit/subscribers","subscription_url":"https://api.github.com/repos/dperpeet/cockpit/subscription","commits_url":"https://api.github.com/repos/dperpeet/cockpit/commits{/sha}","git_commits_url":"https://api.github.com/repos/dperpeet/cockpit/git/commits{/sha}","comments_url":"https://api.github.com/repos/dperpeet/cockpit/comments{/number}","issue_comment_url":"https://api.github.com/repos/dperpeet/cockpit/issues/comments{/number}","contents_url":"https://api.github.com/repos/dperpeet/cockpit/contents/{+path}","compare_url":"https://api.github.com/repos/dperpeet/cockpit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dperpeet/cockpit/merges","archive_url":"https://api.github.com/repos/dperpeet/cockpit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dperpeet/cockpit/downloads","issues_url":"https://api.github.com/repos/dperpeet/cockpit/issues{/number}","pulls_url":"https://api.github.com/repos/dperpeet/cockpit/pulls{/number}","milestones_url":"https://api.github.com/repos/dperpeet/cockpit/milestones{/number}","notifications_url":"https://api.github.com/repos/dperpeet/cockpit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dperpeet/cockpit/labels{/name}","releases_url":"https://api.github.com/repos/dperpeet/cockpit/releases{/id}","deployments_url":"https://api.github.com/repos/dperpeet/cockpit/deployments","created_at":"2014-09-09T15:48:26Z","updated_at":"2016-06-29T12:31:37Z","pushed_at":"2016-07-13T09:31:25Z","git_url":"git://github.com/dperpeet/cockpit.git","ssh_url":"git@github.com:dperpeet/cockpit.git","clone_url":"https://github.com/dperpeet/cockpit.git","svn_url":"https://github.com/dperpeet/cockpit","homepage":"http://www.cockpit-project.org/","size":25729,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":1,"forks":1,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"cockpit-project:master","ref":"master","sha":"3a1a39678f610688f293306ca10d2ebc07ca2263","user":{"login":"cockpit-project","id":5765104,"avatar_url":"https://avatars.githubusercontent.com/u/5765104?v=3","gravatar_id":"","url":"https://api.github.com/users/cockpit-project","html_url":"https://github.com/cockpit-project","followers_url":"https://api.github.com/users/cockpit-project/followers","following_url":"https://api.github.com/users/cockpit-project/following{/other_user}","gists_url":"https://api.github.com/users/cockpit-project/gists{/gist_id}","starred_url":"https://api.github.com/users/cockpit-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cockpit-project/subscriptions","organizations_url":"https://api.github.com/users/cockpit-project/orgs","repos_url":"https://api.github.com/users/cockpit-project/repos","events_url":"https://api.github.com/users/cockpit-project/events{/privacy}","received_events_url":"https://api.github.com/users/cockpit-project/received_events","type":"Organization","site_admin":false},"repo":{"id":14049216,"name":"cockpit","full_name":"cockpit-project/cockpit","owner":{"login":"cockpit-project","id":5765104,"avatar_url":"https://avatars.githubusercontent.com/u/5765104?v=3","gravatar_id":"","url":"https://api.github.com/users/cockpit-project","html_url":"https://github.com/cockpit-project","followers_url":"https://api.github.com/users/cockpit-project/followers","following_url":"https://api.github.com/users/cockpit-project/following{/other_user}","gists_url":"https://api.github.com/users/cockpit-project/gists{/gist_id}","starred_url":"https://api.github.com/users/cockpit-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cockpit-project/subscriptions","organizations_url":"https://api.github.com/users/cockpit-project/orgs","repos_url":"https://api.github.com/users/cockpit-project/repos","events_url":"https://api.github.com/users/cockpit-project/events{/privacy}","received_events_url":"https://api.github.com/users/cockpit-project/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cockpit-project/cockpit","description":"Cockpit, a user interface for Linux servers","fork":false,"url":"https://api.github.com/repos/cockpit-project/cockpit","forks_url":"https://api.github.com/repos/cockpit-project/cockpit/forks","keys_url":"https://api.github.com/repos/cockpit-project/cockpit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cockpit-project/cockpit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cockpit-project/cockpit/teams","hooks_url":"https://api.github.com/repos/cockpit-project/cockpit/hooks","issue_events_url":"https://api.github.com/repos/cockpit-project/cockpit/issues/events{/number}","events_url":"https://api.github.com/repos/cockpit-project/cockpit/events","assignees_url":"https://api.github.com/repos/cockpit-project/cockpit/assignees{/user}","branches_url":"https://api.github.com/repos/cockpit-project/cockpit/branches{/branch}","tags_url":"https://api.github.com/repos/cockpit-project/cockpit/tags","blobs_url":"https://api.github.com/repos/cockpit-project/cockpit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cockpit-project/cockpit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cockpit-project/cockpit/git/refs{/sha}","trees_url":"https://api.github.com/repos/cockpit-project/cockpit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cockpit-project/cockpit/statuses/{sha}","languages_url":"https://api.github.com/repos/cockpit-project/cockpit/languages","stargazers_url":"https://api.github.com/repos/cockpit-project/cockpit/stargazers","contributors_url":"https://api.github.com/repos/cockpit-project/cockpit/contributors","subscribers_url":"https://api.github.com/repos/cockpit-project/cockpit/subscribers","subscription_url":"https://api.github.com/repos/cockpit-project/cockpit/subscription","commits_url":"https://api.github.com/repos/cockpit-project/cockpit/commits{/sha}","git_commits_url":"https://api.github.com/repos/cockpit-project/cockpit/git/commits{/sha}","comments_url":"https://api.github.com/repos/cockpit-project/cockpit/comments{/number}","issue_comment_url":"https://api.github.com/repos/cockpit-project/cockpit/issues/comments{/number}","contents_url":"https://api.github.com/repos/cockpit-project/cockpit/contents/{+path}","compare_url":"https://api.github.com/repos/cockpit-project/cockpit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cockpit-project/cockpit/merges","archive_url":"https://api.github.com/repos/cockpit-project/cockpit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cockpit-project/cockpit/downloads","issues_url":"https://api.github.com/repos/cockpit-project/cockpit/issues{/number}","pulls_url":"https://api.github.com/repos/cockpit-project/cockpit/pulls{/number}","milestones_url":"https://api.github.com/repos/cockpit-project/cockpit/milestones{/number}","notifications_url":"https://api.github.com/repos/cockpit-project/cockpit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cockpit-project/cockpit/labels{/name}","releases_url":"https://api.github.com/repos/cockpit-project/cockpit/releases{/id}","deployments_url":"https://api.github.com/repos/cockpit-project/cockpit/deployments","created_at":"2013-11-01T17:36:46Z","updated_at":"2016-07-13T02:08:29Z","pushed_at":"2016-07-13T13:38:20Z","git_url":"git://github.com/cockpit-project/cockpit.git","ssh_url":"git@github.com:cockpit-project/cockpit.git","clone_url":"https://github.com/cockpit-project/cockpit.git","svn_url":"https://github.com/cockpit-project/cockpit","homepage":"http://www.cockpit-project.org/","size":36979,"stargazers_count":1645,"watchers_count":1645,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":217,"mirror_url":null,"open_issues_count":418,"forks":217,"open_issues":418,"watchers":1645,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715"},"html":{"href":"https://github.com/cockpit-project/cockpit/pull/4715"},"issue":{"href":"https://api.github.com/repos/cockpit-project/cockpit/issues/4715"},"comments":{"href":"https://api.github.com/repos/cockpit-project/cockpit/issues/4715/comments"},"review_comments":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715/comments"},"review_comment":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cockpit-project/cockpit/pulls/4715/commits"},"statuses":{"href":"https://api.github.com/repos/cockpit-project/cockpit/statuses/318962cdf012327af68dc9d0c1fec9cbeae7d3a7"}}}}
{"actor":{"display_login":"dperpeet"}}