Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

Commit 94f7837

Browse files
committed
Implemented namespace deletion
This commit fixes one of the oldest requests we've had: being able to delete a namespace. It also introduces an API endpoint for it. There are two exceptions when it comes to deleting a namespace: 1. Personal namespaces cannot be removed. In the future we might implement user removal, and then it will make sense to re-iterate on this. 2. Global namespaces cannot be removed, because they are meant to be guaranteed by everyone using the registry. Fixes #767 Signed-off-by: Miquel Sabaté Solà <[email protected]> Signed-off-by: Vítor Avelino <[email protected]>
1 parent b07b2aa commit 94f7837

File tree

32 files changed

+665
-41
lines changed

32 files changed

+665
-41
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<template>
2+
<div>
3+
<button
4+
class="btn btn-danger namespace-delete-btn"
5+
data-container="body"
6+
data-placement="left"
7+
data-toggle="popover"
8+
data-content="<p>Are you sure you want to remove this namespace?</p>
9+
<a class='btn btn-default'>No</a> <a class='btn btn-primary yes'>Yes</a>"
10+
data-template="<div class='popover popover-namespace-delete' role='tooltip'><div class='arrow'></div><h3 class='popover-title'></h3><div class='popover-content'></div></div>'"
11+
data-html="true"
12+
role="button"
13+
title="Delete image"
14+
:disabled="state.isDeleting">
15+
<i class="fa fa-trash"></i>
16+
Delete namespace
17+
</button>
18+
</div>
19+
</template>
20+
21+
<script>
22+
import Vue from 'vue';
23+
24+
import { handleHttpResponseError } from '~/utils/http';
25+
26+
import NamespacesStore from '../store';
27+
import NamespacesService from '../services/namespaces';
28+
29+
const { set } = Vue;
30+
31+
export default {
32+
props: {
33+
namespace: {
34+
type: Object,
35+
required: true,
36+
},
37+
redirectPath: String,
38+
},
39+
40+
data() {
41+
return {
42+
state: NamespacesStore.state,
43+
};
44+
},
45+
46+
methods: {
47+
deleteNamespace() {
48+
set(this.state, 'isDeleting', true);
49+
50+
NamespacesService.remove(this.namespace.id).then(() => {
51+
this.$alert.$schedule('Namespace removed with all its repositories');
52+
window.location.href = this.redirectPath;
53+
}).catch(handleHttpResponseError)
54+
.finally(() => set(this.state, 'isDeleting', false));
55+
},
56+
},
57+
58+
mounted() {
59+
const DELETE_BTN = '.namespace-delete-btn';
60+
const POPOVER_DELETE = '.popover-namespace-delete';
61+
62+
// TODO: refactor bootstrap popover to a component
63+
$(this.$el).on('inserted.bs.popover', DELETE_BTN, () => {
64+
const $yes = $(POPOVER_DELETE).find('.yes');
65+
$yes.click(this.deleteNamespace.bind(this));
66+
});
67+
},
68+
};
69+
</script>

app/assets/javascripts/modules/namespaces/pages/show.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<template>
22
<div class="namespaces-show-page">
3+
<div class="header clearfix">
4+
<div class="btn-toolbar pull-right">
5+
<div class="btn-group">
6+
<delete-namespace-btn :namespace="namespace" :redirect-path="namespacesPath" v-if="namespace.destroyable"></delete-namespace-btn>
7+
</div>
8+
</div>
9+
</div>
10+
311
<namespace-details-panel :namespace="namespace" :state="state" :teams-path="teamsPath" :webhooks-path="webhooksPath"></namespace-details-panel>
412
<repositories-panel title="Namespace's repositories" :repositories="repositories" :show-namespaces="false" :repositories-path="repositoriesPath">
513
<div slot="heading-right">
@@ -68,6 +76,7 @@
6876
6977
import RepositoriesPanel from '~/modules/repositories/components/panel';
7078
import WebhooksPanel from '~/modules/webhooks/components/panel';
79+
import DeleteNamespaceBtn from '../components/delete-btn';
7180
7281
import NamespaceDetailsPanel from '../components/details';
7382
@@ -83,6 +92,9 @@
8392
repositoriesRef: {
8493
type: Array,
8594
},
95+
namespacesPath: {
96+
type: String,
97+
},
8698
repositoriesPath: {
8799
type: String,
88100
},
@@ -98,6 +110,7 @@
98110
NamespaceDetailsPanel,
99111
RepositoriesPanel,
100112
WebhooksPanel,
113+
DeleteNamespaceBtn,
101114
},
102115
103116
data() {

app/assets/javascripts/modules/namespaces/services/namespaces.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,16 @@ function teamExists(value) {
5858
.catch(() => null);
5959
}
6060

61+
function remove(id) {
62+
return resource.delete({ id });
63+
}
64+
6165
export default {
6266
get,
6367
all,
6468
update,
6569
save,
70+
remove,
6671
searchTeam,
6772
teamExists,
6873
validate,

app/assets/javascripts/modules/namespaces/store.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ class RepositoriesStore {
33
this.state = {
44
newFormVisible: false,
55
editFormVisible: false,
6+
isDeleting: false,
67
isLoading: false,
78
notLoaded: false,
89
onGoingVisibilityRequest: false,

app/assets/javascripts/modules/users/components/application-tokens/panel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<h5 slot="heading-left">Application tokens</h5>
44

55
<div slot="heading-right" v-if="canCreate">
6-
<toggle-link text="Create new team" :state="state" state-key="newFormVisible" class="toggle-link-new-app-token"></toggle-link>
6+
<toggle-link text="Create new token" :state="state" state-key="newFormVisible" class="toggle-link-new-app-token"></toggle-link>
77
</div>
88

99
<div slot="body">

app/helpers/namespaces_helper.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,43 @@ module NamespacesHelper
99
def can_create_namespace?
1010
current_user.admin? || APP_CONFIG.enabled?("user_permission.create_namespace")
1111
end
12+
13+
# Render the name for the namespace from the given activity. An article can be
14+
# prepended to the activity if `article` is set to true (e.g. "deleted the
15+
# namespace" vs "deleted namespace").
16+
#
17+
# NOTE: to be removed when working on #1936,
18+
def render_namespace_name(activity, article = false)
19+
name = activity.parameters[:namespace_name]
20+
21+
if name
22+
articled("the ", name, article)
23+
elsif activity.trackable && activity.trackable_type == "Namespace"
24+
articled("the ", link_to(activity.trackable.name, activity.trackable), article)
25+
else
26+
articled(name, "a", article)
27+
end
28+
end
29+
30+
# Render the team for the namespace from the given activity.
31+
#
32+
# NOTE: to be removed when working on #1936,
33+
def render_namespace_team(activity)
34+
name = activity.parameters[:team]
35+
36+
if activity.trackable_type == "Namespace" && activity.trackable&.team
37+
content_tag(:span, "the ") + link_to(activity.trackable.team.name, activity.trackable.team)
38+
elsif name
39+
content_tag(:span, "the ") + name
40+
else
41+
content_tag(:span, "a")
42+
end
43+
end
44+
45+
protected
46+
47+
# NOTE: to be removed when working on #1936,
48+
def articled(pre, post, article)
49+
article ? content_tag(:span, pre) + post : post
50+
end
1251
end

app/helpers/repositories_helper.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def render_repo_activity(activity, action)
6565
owner = content_tag(:strong, "#{activity_owner(activity)} #{action} ")
6666

6767
namespace = render_namespace(activity)
68-
namespace += " / " unless namespace.empty?
68+
namespace += "/" unless namespace.empty?
6969

7070
owner + namespace + render_repository(activity)
7171
end
@@ -78,7 +78,7 @@ def render_repo_activity(activity, action)
7878
def render_namespace(activity)
7979
tr = activity.trackable
8080

81-
if tr.nil? || tr.is_a?(Namespace)
81+
if tr.nil? || tr.is_a?(Namespace) || tr.is_a?(Registry)
8282
if activity.parameters[:namespace_name].nil?
8383
""
8484
else
@@ -111,7 +111,7 @@ def render_repository(activity)
111111
def get_repo_link_tag(activity)
112112
tr = activity.trackable
113113

114-
if tr.nil?
114+
if tr.nil? || tr.is_a?(Registry)
115115
if repo_name(activity).nil?
116116
["a repository", nil, ""]
117117
else

app/models/activity/fallback.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
# Activity holds utility methods to update activity records depending on the
4+
# model including this module.
5+
module Activity
6+
# Fallback has a set of methods that manage trackable types for existing
7+
# activities.
8+
module Fallback
9+
# fallback_activity updates the model including this method by setting a new
10+
# type an id for all the rows.
11+
def fallback_activity(type, id)
12+
PublicActivity::Activity.where(trackable: self).update_all(
13+
trackable_type: type,
14+
trackable_id: id,
15+
recipient_type: nil
16+
)
17+
end
18+
end
19+
end

app/models/namespace.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
class Namespace < ActiveRecord::Base
2525
include PublicActivity::Common
2626
include SearchCop
27+
include ::Activity::Fallback
2728

2829
search_scope :search do
2930
attributes :name, :description
@@ -122,7 +123,6 @@ def self.make_valid(name)
122123
# To avoid any name conflict we append an incremental number to the end
123124
# of the name returns it as the name that will be used on both Namespace
124125
# and Team on the User#create_personal_namespace! method
125-
# TODO: workaround until we implement the namespace/team removal
126126
increment = 0
127127
original_name = name
128128
while Namespace.exists?(name: name)
@@ -140,4 +140,40 @@ def self.make_valid(name)
140140
def clean_name
141141
global? ? registry.hostname : name
142142
end
143+
144+
# Tries to delete a namespace and, on success, it will create delete
145+
# activities and update related ones. This method assumes that all
146+
# repositories and tags under this namespace have already been destroyed.
147+
def delete_by!(actor)
148+
destroy ? create_delete_activities!(actor) : false
149+
end
150+
151+
protected
152+
153+
def create_delete_activities!(actor)
154+
# Set the namespace name in the parameters field.
155+
# TODO(2.5): this could be more performant in PostgreSQL if we used its
156+
# `jsonb_set` function. Plus, in Rails 5 there might be some improvements
157+
# that might help on this.
158+
ActiveRecord::Base.transaction do
159+
PublicActivity::Activity.where(trackable: self).find_each do |act|
160+
act.parameters[:namespace_name] = clean_name
161+
act.save
162+
end
163+
end
164+
165+
fallback_activity(Registry, registry.id)
166+
167+
# Add a "delete" activity"
168+
registry.create_activity(
169+
:delete,
170+
owner: actor,
171+
recipient: self,
172+
parameters: {
173+
namespace_name: clean_name,
174+
team_name: team.name,
175+
registry_id: registry.id
176+
}
177+
)
178+
end
143179
end

app/models/registry.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
# NOTE: currently only one Registry is allowed to exist in the database. This
2525
# might change in the future.
2626
class Registry < ActiveRecord::Base
27+
include PublicActivity::Common
28+
2729
has_many :namespaces, dependent: :destroy
2830

2931
validates :name, presence: true, uniqueness: true

0 commit comments

Comments
 (0)