← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:ocirecipe-sharing-lists into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ocirecipe-sharing-lists into launchpad:master with ~pappacena/launchpad:ocirecipe-edit-info-type-ui as a prerequisite.

Commit message:
Showing snaps and ocirecipes as shareable artifacts on +sharing pages

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/400059
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ocirecipe-sharing-lists into launchpad:master.
diff --git a/lib/lp/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py
index c5bfd3f..739b56f 100644
--- a/lib/lp/registry/browser/pillar.py
+++ b/lib/lp/registry/browser/pillar.py
@@ -422,6 +422,9 @@ class PillarPersonSharingView(LaunchpadView):
         bug_data = self._build_bug_template_data(self.bugtasks, request)
         spec_data = self._build_specification_template_data(
             self.specifications, request)
+        snap_data = self._build_ocirecipe_template_data(self.snaps, request)
+        ocirecipe_data = self._build_ocirecipe_template_data(
+            self.ocirecipes, request)
         grantee_data = {
             'displayname': self.person.displayname,
             'self_link': absoluteURL(self.person, request)
@@ -435,6 +438,8 @@ class PillarPersonSharingView(LaunchpadView):
         cache.objects['branches'] = branch_data
         cache.objects['gitrepositories'] = gitrepository_data
         cache.objects['specifications'] = spec_data
+        cache.objects['snaps'] = snap_data
+        cache.objects['ocirecipes'] = ocirecipe_data
 
     def _loadSharedArtifacts(self):
         # As a concrete can by linked via more than one policy, we use sets to
@@ -504,3 +509,25 @@ class PillarPersonSharingView(LaunchpadView):
                 bug_importance=importance,
                 information_type=information_type))
         return bug_data
+
+    def _build_ocirecipe_template_data(self, oci_recipes, request):
+        recipe_data = []
+        for recipe in oci_recipes:
+            recipe_data.append(dict(
+                self_link=absoluteURL(recipe, request),
+                web_link=canonical_url(recipe, path_only_if_possible=True),
+                name=recipe.name,
+                id=recipe.id,
+                information_type=recipe.information_type.title))
+        return recipe_data
+
+    def _build_snap_template_data(self, snaps, request):
+        snap_data = []
+        for snap in snaps:
+            snap_data.append(dict(
+                self_link=absoluteURL(snap, request),
+                web_link=canonical_url(snap, path_only_if_possible=True),
+                name=snap.name,
+                id=snap.id,
+                information_type=snap.information_type.title))
+        return snap_data
diff --git a/lib/lp/registry/browser/tests/test_pillar_sharing.py b/lib/lp/registry/browser/tests/test_pillar_sharing.py
index 493b42b..215f946 100644
--- a/lib/lp/registry/browser/tests/test_pillar_sharing.py
+++ b/lib/lp/registry/browser/tests/test_pillar_sharing.py
@@ -172,8 +172,9 @@ class PillarSharingDetailsMixin:
             pillarperson.pillar.name, pillarperson.person.name)
         browser = self.getUserBrowser(user=self.owner, url=url)
         self.assertIn(
-            'There are no shared bugs, Bazaar branches, Git repositories, or '
-            'blueprints.', normalize_whitespace(browser.contents))
+            'There are no shared bugs, Bazaar branches, Git repositories, '
+            'Snaps, OCI recipes or blueprints.',
+            normalize_whitespace(browser.contents))
 
     def test_init_works(self):
         # The view works with a feature flag.
diff --git a/lib/lp/registry/javascript/sharing/sharingdetails.js b/lib/lp/registry/javascript/sharing/sharingdetails.js
index 0ac4f32..60b56d5 100644
--- a/lib/lp/registry/javascript/sharing/sharingdetails.js
+++ b/lib/lp/registry/javascript/sharing/sharingdetails.js
@@ -1,4 +1,4 @@
-/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE).
  *
  * Sharing details widget
@@ -148,6 +148,58 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         ].join(' ');
     },
 
+    _snap_details_row_template: function() {
+        return [
+        '<tr id="shared-snap-{{id}}">',
+        '    <td>',
+        '        <span class="sortkey">{{id}}</span>',
+        '        <a class="sprite source-package-recipe" href="{{web_link}}">',
+        '            {{name}}',
+        '        </a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-snap-{{id}}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare Snap {{name}} with {{displayname}}"',
+        '        data-self_link="{{self_link}}" data-name="{{name}}"',
+        '        data-type="snap">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
+    _ocirecipe_details_row_template: function() {
+        return [
+        '<tr id="shared-ocirecipe-{{id}}">',
+        '    <td>',
+        '        <span class="sortkey">{{id}}</span>',
+        '        <a class="sprite source-package-recipe" href="{{web_link}}">',
+        '            {{name}}',
+        '        </a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-ocirecipe-{{id}}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare OCI recipe {{name}} with {{displayname}}"',
+        '        data-self_link="{{self_link}}" data-name="{{name}}"',
+        '        data-type="ocirecipe">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
     _table_body_template: function() {
         return [
         '<tbody id="sharing-table-body">',
@@ -160,6 +212,12 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         '{{#gitrepositories}}',
         '{{> gitrepository}}',
         '{{/gitrepositories}}',
+        '{{#snaps}}',
+        '{{> snap}}',
+        '{{/snaps}}',
+        '{{#ocirecipes}}',
+        '{{> ocirecipe}}',
+        '{{/ocirecipes}}',
         '{{#specifications}}',
         '{{> spec}}',
         '{{/specifications}}',
@@ -183,7 +241,7 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
 
     // Delete the specified grantees from the table.
     delete_artifacts: function(bugs, branches, gitrepositories, specifications,
-                               all_rows_deleted) {
+                               snaps, ocirecipes, all_rows_deleted) {
         var deleted_row_selectors = [];
         var details_table_body = this.get('details_table_body');
         Y.Array.each(bugs, function(bug) {
@@ -215,6 +273,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
                 deleted_row_selectors.push(selector);
             }
         });
+        Y.Array.each(snaps, function(snap) {
+            var selector = 'tr[id=shared-snap-' + snap.id + ']';
+            var table_row = details_table_body.one(selector);
+            if (Y.Lang.isValue(table_row)) {
+                deleted_row_selectors.push(selector);
+            }
+        });
+        Y.Array.each(ocirecipes, function(ocirecipe) {
+            var selector = 'tr[id=shared-ocirecipe-' + ocirecipe.id + ']';
+            var table_row = details_table_body.one(selector);
+            if (Y.Lang.isValue(table_row)) {
+                deleted_row_selectors.push(selector);
+            }
+        });
 
         if (deleted_row_selectors.length === 0) {
             return;
@@ -232,7 +304,7 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
                     .appendChild('<td colspan="3"></td>')
                     .setContent(
                         "There are no shared bugs, Bazaar branches, " +
-                        "Git repositories, or blueprints.");
+                        "Git repositories, Snaps, OCI recipes or blueprints.");
             }
         };
         var anim_duration = this.get('anim_duration');
@@ -275,6 +347,12 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         this.set(
             'spec_details_row_template',
             this._spec_details_row_template());
+        this.set(
+            'snap_details_row_template',
+            this._snap_details_row_template());
+        this.set(
+            'ocirecipe_details_row_template',
+            this._ocirecipe_details_row_template());
 
         this.set(
             'table_body_template',
@@ -288,16 +366,21 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         var gitrepositories = this.get('gitrepositories');
         var bugs = this.get('bugs');
         var specs = this.get('specifications');
+        var snaps = this.get('snaps');
+        var ocirecipes = this.get('ocirecipes');
 
         if (bugs.length === 0 && branches.length === 0 &&
-            gitrepositories.length === 0 && specs.length === 0 ) {
+            gitrepositories.length === 0 && specs.length === 0 &&
+            snaps.length === 0 && ocirecipes.length === 0) {
             return;
         }
         var partials = {
             branch: this.get('branch_details_row_template'),
             gitrepository: this.get('gitrepository_details_row_template'),
             bug: this.get('bug_details_row_template'),
-            spec: this.get('spec_details_row_template')
+            spec: this.get('spec_details_row_template'),
+            snap: this.get('snap_details_row_template'),
+            ocirecipe: this.get('ocirecipe_details_row_template')
         };
         var template = this.get('table_body_template');
         var html = Y.lp.mustache.to_html(
@@ -307,6 +390,8 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
                 gitrepositories: gitrepositories,
                 bugs: bugs,
                 specifications: specs,
+                snaps: snaps,
+                ocirecipes: ocirecipes,
                 displayname: this.get('person_name')
             },
             partials);
@@ -344,14 +429,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         var existing_branches = this.get('branches');
         var existing_gitrepositories = this.get('gitrepositories');
         var existing_specifications = this.get('specifications');
+        var existing_snaps = this.get('snaps');
+        var existing_ocirecipes = this.get('ocirecipes');
         var model_bugs = LP.cache.bugs;
         var model_branches = LP.cache.branches;
         var model_gitrepositories = LP.cache.gitrepositories;
         var model_specifications = LP.cache.specifications;
+        var model_snaps = LP.cache.snaps;
+        var model_ocirecipes = LP.cache.ocirecipes;
         var deleted_bugs = [];
         var deleted_branches = [];
         var deleted_gitrepositories = [];
         var deleted_specifications = [];
+        var deleted_snaps = [];
+        var deleted_ocirecipes = [];
 
         var self = this;
         Y.Array.each(existing_bugs, function(bug) {
@@ -388,12 +479,29 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
             }
         });
 
+        Y.Array.each(existing_snaps, function(snap) {
+            var model_snap = self._get_artifact_from_model(
+                    snap.id, 'id', model_snaps);
+            if (!Y.Lang.isValue(model_snap)) {
+                deleted_snaps.push(snap);
+            }
+        });
+
+        Y.Array.each(existing_ocirecipes, function(ocirecipe) {
+            var model_ocirecipe = self._get_artifact_from_model(
+                    ocirecipe.id, 'id', model_ocirecipes);
+            if (!Y.Lang.isValue(model_ocirecipe)) {
+                deleted_ocirecipes.push(ocirecipe);
+            }
+        });
+
         if (deleted_bugs.length > 0 || deleted_branches.length > 0 ||
             deleted_gitrepositories.length > 0 ||
-            deleted_specifications.length > 0) {
+            deleted_specifications.length > 0 || deleted_snaps.length > 0 ||
+            deleted_ocirecipes.length > 0) {
             this.delete_artifacts(
                 deleted_bugs, deleted_branches, deleted_gitrepositories,
-                deleted_specifications,
+                deleted_specifications, deleted_snaps, deleted_ocirecipes,
                 model_bugs.length === 0 && model_branches.length === 0 &&
                 model_gitrepositories.length === 0 &&
                 deleted_specifications.length === 0);
@@ -403,6 +511,8 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
         this.set('branches', model_branches);
         this.set('gitrepositories', model_gitrepositories);
         this.set('specifications', model_specifications);
+        this.set('snaps', model_snaps);
+        this.set('ocirecipes', model_ocirecipes);
 
         Y.lp.app.sorttable.SortTable.registerSortKeyFunction(
             'branchsortkey', this.branch_sort_key);
@@ -433,6 +543,14 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
                 value: null
             },
 
+            snap_details_row_template: {
+                value: null
+            },
+
+            ocirecipe_details_row_template: {
+                value: null
+            },
+
             branches: {
                 value: [],
                 // We clone the data passed in so external modifications do not
@@ -454,6 +572,20 @@ ns.SharingDetailsTable = Y.Base.create('sharingDetailsTable', Y.Widget, [], {
                 setter: clone_data
             },
 
+            snaps: {
+                value: [],
+                // We clone the data passed in so external modifications do not
+                // interfere.
+                setter: clone_data
+            },
+
+            ocirecipes: {
+                value: [],
+                // We clone the data passed in so external modifications do not
+                // interfere.
+                setter: clone_data
+            },
+
             // The node holding the details table.
             details_table_body: {
                 getter: function() {
diff --git a/lib/lp/registry/javascript/sharing/sharingdetailsview.js b/lib/lp/registry/javascript/sharing/sharingdetailsview.js
index b41e9fe..d4197be 100644
--- a/lib/lp/registry/javascript/sharing/sharingdetailsview.js
+++ b/lib/lp/registry/javascript/sharing/sharingdetailsview.js
@@ -1,4 +1,4 @@
-/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE).
  *
  * Disclosure infrastructure.
@@ -32,6 +32,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
             bugs: LP.cache.bugs,
             branches: LP.cache.branches,
             gitrepositories: LP.cache.gitrepositories,
+            snaps: LP.cache.snaps,
+            ocirecipes: LP.cache.ocirecipes,
             person_name: LP.cache.grantee.displayname,
             specifications: LP.cache.specifications,
             write_enabled: true
@@ -85,6 +87,22 @@ Y.extend(SharingDetailsView, Y.Widget, {
                     }
                 });
                 break;
+            case 'snap':
+                Y.Array.some(LP.cache.snaps, function(snap) {
+                    if (snap.self_link === artifact_uri) {
+                        artifact_id = snap.id;
+                        return true;
+                    }
+                });
+                break;
+            case 'ocirecipe':
+                Y.Array.some(LP.cache.ocirecipes, function(ocirecipe) {
+                    if (ocirecipe.self_link === artifact_uri) {
+                        artifact_id = ocirecipe.id;
+                        return true;
+                    }
+                });
+                break;
             case 'spec':
                 Y.Array.some(LP.cache.specifications, function(spec) {
                     if (spec.self_link === artifact_uri) {
@@ -207,6 +225,22 @@ Y.extend(SharingDetailsView, Y.Widget, {
                 return true;
             }
         });
+        var snap_data = LP.cache.snaps;
+        Y.Array.some(snap_data, function(snap, index) {
+            if (snap.self_link === artifact_uri) {
+                snap_data.splice(index, 1);
+                self.syncUI();
+                return true;
+            }
+        });
+        var ocirecipe_data = LP.cache.ocirecipes;
+        Y.Array.some(ocirecipe_data, function(ocirecipe, index) {
+            if (ocirecipe.self_link === artifact_uri) {
+                ocirecipe_data.splice(index, 1);
+                self.syncUI();
+                return true;
+            }
+        });
     },
 
     /**
@@ -223,6 +257,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
         var branches = [];
         var gitrepositories = [];
         var specifications = [];
+        var snaps = [];
+        var ocirecipes = [];
         switch (artifact_type) {
             case 'bug':
                 bugs = [artifact_uri];
@@ -233,6 +269,12 @@ Y.extend(SharingDetailsView, Y.Widget, {
             case 'gitrepository':
                 gitrepositories = [artifact_uri];
                 break;
+            case 'snap':
+                snaps = [artifact_uri];
+                break;
+            case 'ocirecipe':
+                ocirecipes = [artifact_uri];
+                break;
             case 'spec':
                 specifications = [artifact_uri];
                 break;
@@ -256,6 +298,8 @@ Y.extend(SharingDetailsView, Y.Widget, {
                 bugs: bugs,
                 branches: branches,
                 gitrepositories: gitrepositories,
+                snaps: snaps,
+                ocirecipes: ocirecipes,
                 specifications: specifications
             }
         };
diff --git a/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js b/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
index 7b7d306..30ee62a 100644
--- a/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
+++ b/lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js
@@ -1,4 +1,4 @@
-/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE). */
 YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
 
@@ -44,6 +44,26 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
                             repository_name: '~someone/+git/somerepo',
                             information_type: 'Private'
                         }
+                    ],
+                    snaps: [
+                        {
+                            self_link: 'api/devel/~someone/+snap/somesnap',
+                            web_link: '/~someone/+snap/somesnap',
+                            id: '2',
+                            name: 'snap-name',
+                            information_type: 'Private'
+                        }
+                    ],
+                    ocirecipes: [
+                        {
+                            self_link: ('api/devel/~someone/proj/+oci/' +
+                                        'ociproj/+recipe/recipe'),
+                            web_link: ('~someone/proj/+oci/' +
+                                       'ociproj/+recipe/recipe'),
+                            name: 'ocirecipe-name',
+                            id: '2',
+                            information_type: 'Private'
+                        }
                     ]
                 }
             };
@@ -71,6 +91,8 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
                 bugs: window.LP.cache.bugs,
                 branches: window.LP.cache.branches,
                 gitrepositories: window.LP.cache.gitrepositories,
+                snaps: window.LP.cache.snaps,
+                ocirecipes: window.LP.cache.ocirecipes,
                 write_enabled: true
             }, overrides);
             window.LP.cache.grantee_data = config.grantees;
@@ -326,10 +348,13 @@ YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
                 [window.LP.cache.bugs[0]],
                 [window.LP.cache.branches[0]],
                 [window.LP.cache.gitrepositories[0]],
+                [], // specs
+                [window.LP.cache.snaps[0]],
+                [window.LP.cache.ocirecipes[0]],
                 true);
             Y.Assert.areEqual(
                 'There are no shared bugs, Bazaar branches, Git ' +
-                'repositories, or blueprints.',
+                'repositories, Snaps, OCI recipes or blueprints.',
                 Y.one('#sharing-table-body tr').get('text'));
         },
 
diff --git a/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js b/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
index 84f7930..bdcdcfb 100644
--- a/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
+++ b/lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js
@@ -1,4 +1,4 @@
-/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE). */
 
 YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
@@ -49,6 +49,26 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
                             web_link: "/obsolete-junk/+spec/big-project"
                         }
                     ],
+                    snaps: [
+                        {
+                            self_link: 'api/devel/~someone/+snap/somesnap',
+                            web_link: '/~someone/+snap/somesnap',
+                            id: '2',
+                            name: 'snap-name',
+                            information_type: 'Private'
+                        }
+                    ],
+                    ocirecipes: [
+                        {
+                            self_link: ('api/devel/~someone/proj/+oci/' +
+                                        'ociproj/+recipe/recipe'),
+                            web_link: ('~someone/proj/+oci/' +
+                                       'ociproj/+recipe/recipe'),
+                            name: 'ocirecipe-name',
+                            id: '2',
+                            information_type: 'Private'
+                        }
+                    ],
                     grantee: {
                         displayname: 'Fred Bloggs',
                         self_link: '~fred'
@@ -185,6 +205,51 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
             Y.Assert.isTrue(confirmRemove_called);
         },
 
+        // Clicking a Snap remove link calls the
+        // confirm_grant_removal method with the correct parameters.
+        test_remove_snap_grant_click: function() {
+            this.view = this._create_Widget();
+            this.view.render();
+            var confirmRemove_called = false;
+            this.view.confirm_grant_removal = function(
+                    delete_link, artifact_uri, artifact_name, artifact_type) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/+snap/somesnap', artifact_uri);
+                Y.Assert.areEqual('snap-name', artifact_name);
+                Y.Assert.areEqual('snap', artifact_type);
+                Y.Assert.areEqual(delete_link_to_click, delete_link);
+                confirmRemove_called = true;
+
+            };
+            var delete_link_to_click =
+                Y.one('#sharing-table-body span[id=remove-snap-2] a');
+            delete_link_to_click.simulate('click');
+            Y.Assert.isTrue(confirmRemove_called);
+        },
+
+        // Clicking an OCI recipe remove link calls the
+        // confirm_grant_removal method with the correct parameters.
+        test_remove_ocirecipe_grant_click: function() {
+            this.view = this._create_Widget();
+            this.view.render();
+            var confirmRemove_called = false;
+            this.view.confirm_grant_removal = function(
+                    delete_link, artifact_uri, artifact_name, artifact_type) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
+                    artifact_uri);
+                Y.Assert.areEqual('ocirecipe-name', artifact_name);
+                Y.Assert.areEqual('ocirecipe', artifact_type);
+                Y.Assert.areEqual(delete_link_to_click, delete_link);
+                confirmRemove_called = true;
+
+            };
+            var delete_link_to_click =
+                Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
+            delete_link_to_click.simulate('click');
+            Y.Assert.isTrue(confirmRemove_called);
+        },
+
         //Test the behaviour of the removal confirmation dialog.
         _test_confirm_grant_removal: function(click_ok) {
             this.view = this._create_Widget();
@@ -342,6 +407,88 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
             Y.Assert.isTrue(remove_grant_success_called);
         },
 
+        // The perform_remove_grant method makes the expected XHR calls when a
+        // Snap grant remove link is clicked.
+        test_perform_remove_snap_grant: function() {
+            var mockio = new Y.lp.testing.mockio.MockIo();
+            var lp_client = new Y.lp.client.Launchpad({
+                io_provider: mockio
+            });
+            this.view = this._create_Widget({
+                lp_client: lp_client
+            });
+            this.view.render();
+            var remove_grant_success_called = false;
+            this.view.remove_grant_success = function(artifact_uri) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/+snap/somesnap', artifact_uri);
+                remove_grant_success_called = true;
+            };
+            var delete_link =
+                Y.one('#sharing-table-body span[id=remove-snap-2] a');
+            this.view.perform_remove_grant(
+                delete_link, 'api/devel/~someone/+snap/somesnap',
+                'snap');
+            Y.Assert.areEqual(
+                '/api/devel/+services/sharing',
+                mockio.last_request.url);
+            var expected_qs = '';
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'ws.op', 'revokeAccessGrants');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'pillar', '/pillar');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'grantee', '~fred');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'snaps',
+                'api/devel/~someone/+snap/somesnap');
+            Y.Assert.areEqual(expected_qs, mockio.last_request.config.data);
+            mockio.last_request.successJSON({});
+            Y.Assert.isTrue(remove_grant_success_called);
+        },
+
+        // The perform_remove_grant method makes the expected XHR calls when an
+        // OCI recipe grant remove link is clicked.
+        test_perform_remove_ocirecipe_grant: function() {
+            var mockio = new Y.lp.testing.mockio.MockIo();
+            var lp_client = new Y.lp.client.Launchpad({
+                io_provider: mockio
+            });
+            this.view = this._create_Widget({
+                lp_client: lp_client
+            });
+            this.view.render();
+            var remove_grant_success_called = false;
+            this.view.remove_grant_success = function(artifact_uri) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
+                    artifact_uri);
+                remove_grant_success_called = true;
+            };
+            var delete_link =
+                Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
+            this.view.perform_remove_grant(
+                delete_link,
+                'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
+                'ocirecipe');
+            Y.Assert.areEqual(
+                '/api/devel/+services/sharing',
+                mockio.last_request.url);
+            var expected_qs = '';
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'ws.op', 'revokeAccessGrants');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'pillar', '/pillar');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'grantee', '~fred');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'ocirecipes',
+                'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe');
+            Y.Assert.areEqual(expected_qs, mockio.last_request.config.data);
+            mockio.last_request.successJSON({});
+            Y.Assert.isTrue(remove_grant_success_called);
+        },
+
         // The remove bug grant callback updates the model and syncs the UI.
         test_remove_bug_grant_success: function() {
             this.view = this._create_Widget({anim_duration: 0});
@@ -410,6 +557,40 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
                 'All specs are removed from the cache.');
         },
 
+        // The remove Snap grant callback updates the model and
+        // syncs the UI.
+        test_remove_snap_grant_success: function() {
+            this.view = this._create_Widget({anim_duration: 0});
+            this.view.render();
+            var syncUI_called = false;
+            this.view.syncUI = function() {
+                syncUI_called = true;
+            };
+            this.view.remove_grant_success(
+                'api/devel/~someone/+snap/somesnap');
+            Y.Assert.isTrue(syncUI_called);
+            Y.Array.each(window.LP.cache.snaps, function(snap) {
+                Y.Assert.areNotEqual(2, snap.id);
+            });
+        },
+
+        // The remove OCI recipe grant callback updates the model and
+        // syncs the UI.
+        test_remove_ocirecipe_grant_success: function() {
+            this.view = this._create_Widget({anim_duration: 0});
+            this.view.render();
+            var syncUI_called = false;
+            this.view.syncUI = function() {
+                syncUI_called = true;
+            };
+            this.view.remove_grant_success(
+                'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe');
+            Y.Assert.isTrue(syncUI_called);
+            Y.Array.each(window.LP.cache.ocirecipes, function(recipe) {
+                Y.Assert.areNotEqual(2, recipe.id);
+            });
+        },
+
         // XHR calls display errors correctly.
         _assert_error_displayed_on_failure: function(
                 artifact, invoke_operation) {
@@ -491,6 +672,34 @@ YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
             };
             this._assert_error_displayed_on_failure('spec', invoke_remove);
         },
+
+        // The perform_remove_grant method handles errors correctly with Snaps.
+        test_perform_remove_snap_error: function() {
+            var invoke_remove = function(view) {
+                var delete_link =
+                    Y.one('#sharing-table-body span[id=remove-snap-2] a');
+                    view.perform_remove_grant(
+                        delete_link, 'api/devel/~someone/+snap/somesnap',
+                        'snap');
+            };
+            this._assert_error_displayed_on_failure('snap', invoke_remove);
+        },
+
+        // The perform_remove_grant method handles errors correctly with OCI
+        // recipes.
+        test_perform_remove_ocirecipe_error: function() {
+            var invoke_remove = function(view) {
+                var delete_link =
+                    Y.one('#sharing-table-body span[id=remove-ocirecipe-2] a');
+                    view.perform_remove_grant(
+                        delete_link,
+                        'api/devel/~someone/proj/+oci/ociproj/+recipe/recipe',
+                        'ocirecipe');
+            };
+            this._assert_error_displayed_on_failure(
+                'ocirecipe', invoke_remove);
+        },
+
         // Test that syncUI works as expected.
         test_syncUI: function() {
             this.view = this._create_Widget();
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index 4927dc4..f52575c 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/sharingservice.py
@@ -865,7 +865,7 @@ class SharingService:
 
         # Create a job to remove subscriptions for artifacts the grantee can no
         # longer see.
-        return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
+        getUtility(IRemoveArtifactSubscriptionsJobSource).create(
             user, artifacts, grantee=grantee, pillar=pillar)
 
     def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
diff --git a/lib/lp/registry/templates/pillar-sharing-details.pt b/lib/lp/registry/templates/pillar-sharing-details.pt
index a6f2710..0b89496 100644
--- a/lib/lp/registry/templates/pillar-sharing-details.pt
+++ b/lib/lp/registry/templates/pillar-sharing-details.pt
@@ -26,20 +26,31 @@
   <div metal:fill-slot="main">
 
     <div id="observer-summary">
-      <p>
-      <tal:bugs replace="view/shared_bugs_count">0</tal:bugs> bugs,
-      <tal:branches replace="view/shared_branches_count">0</tal:branches> Bazaar branches,
-      <tal:gitrepositories replace="view/shared_gitrepositories_count">0</tal:gitrepositories> Git repositories,
-      and  <tal:specifications
-      replace="view/shared_specifications_count">0</tal:specifications>
-      blueprints shared with <tal:name replace="view/person/displayname">
-      grantee</tal:name>.<br />
-
       <tal:is-team condition="view/person/is_team">
         <tal:members>3</tal:members> team members can view these bugs,
-        Bazaar branches, Git repositories, and blueprints.
+        Bazaar branches, Git repositories, Snaps, OCI recipes and blueprints.
       </tal:is-team>
-      </p>
+      Shared with <tal:name replace="view/person/displayname">grantee</tal:name>
+      <ul class="listing">
+        <li tal:condition="view/shared_bugs_count">
+          <span tal:replace="view/shared_bugs_count" /> Bugs
+        </li>
+        <li tal:condition="view/shared_branches_count">
+          <span tal:replace="view/shared_branches_count" /> Bazaar branches
+        </li>
+        <li tal:condition="view/shared_gitrepositories_count">
+          <span tal:replace="view/shared_gitrepositories_count" /> Git repositories
+        </li>
+        <li tal:condition="view/shared_ocirecipe_count">
+          <span tal:replace="view/shared_ocirecipe_count" /> OCI recipes
+        </li>
+        <li tal:condition="view/shared_snaps_count">
+          <span tal:replace="view/shared_snaps_count" /> Snap recipes
+        </li>
+        <li tal:condition="view/shared_specifications_count">
+          <span tal:replace="view/shared_specifications_count" /> Blueprints
+        </li>
+      </ul>
     </div>
 
     <table id="shared-table" class="listing sortable">
@@ -49,7 +60,8 @@
       <thead>
         <tr>
           <th colspan="2" width="">
-            Subscribed Bug Report, Bazaar Branch, Git Repository, or Blueprint
+            Subscribed Bug Report, Bazaar Branch, Git Repository, Snaps, OCI
+             recipes or Blueprint
           </th>
           <th>
             Information Type
@@ -60,7 +72,7 @@
           <tr>
               <td colspan="3">
                   There are no shared bugs, Bazaar branches, Git repositories,
-                  or blueprints.
+                  Snaps, OCI recipes or blueprints.
               </td>
           </tr>
       </tbody>

Follow ups