Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
synnefo
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
itminedu
synnefo
Commits
373da652
Commit
373da652
authored
Feb 06, 2014
by
Kostas Papadimitriou
Committed by
Giorgos Korfiatis
Feb 13, 2014
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
astakos: Display both summed up and per project quota in usage view
parent
38fc975e
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
240 additions
and
51 deletions
+240
-51
snf-astakos-app/astakos/im/static/im/css/modules.css
snf-astakos-app/astakos/im/static/im/css/modules.css
+7
-3
snf-astakos-app/astakos/im/static/im/js/usage.js
snf-astakos-app/astakos/im/static/im/js/usage.js
+174
-27
snf-astakos-app/astakos/im/templates/im/resource_usage.html
snf-astakos-app/astakos/im/templates/im/resource_usage.html
+45
-16
snf-astakos-app/astakos/im/views/im.py
snf-astakos-app/astakos/im/views/im.py
+14
-5
No files found.
snf-astakos-app/astakos/im/static/im/css/modules.css
View file @
373da652
...
...
@@ -522,9 +522,9 @@ form input[type="text"]:-ms-input-placeholder,
.stats
.bar
span
+
em
{
position
:
absolute
;
}
.stats
.bar
{
position
:
relative
;
}
.stats
.re
d
.bar
span
{
background
:
#ef4f54
;
}
.stats
.
yellow
.bar
span
{
background
:
#f6921e
;
}
.stats
.
green
.bar
span
{
background
:
#55b577
;
}
.stats
.re
source-bar.red
.bar
span
{
background
:
#ef4f54
;
}
.stats
.
resource-bar.yellow
.bar
span
{
background
:
#f6921e
;
}
.stats
.
resource-bar.green
.bar
span
{
background
:
#55b577
;
}
.stats
.img-wrap
{
float
:
left
;
width
:
100px
;
background
:
url(../images/statistics_icons.png)
no-repeat
center
center
;
padding
:
30px
0
;
}
.stats
.info
{
margin
:
0
25px
;
width
:
320px
;
float
:
left
;
}
.stats
.info
p
{
color
:
#999
;
margin
:
0
;
}
...
...
@@ -546,6 +546,10 @@ form input[type="text"]:-ms-input-placeholder,
.stats
.red
.img-wrap
{
background-position
:
15px
7px
;
}
.stats
.yellow
.img-wrap
{
background-position
:
-124px
7px
;
}
.stats
.green
.img-wrap
{
background-position
:
-263px
7px
;
}
.stats
.resource-bar.project
{
margin-top
:
1em
;
}
.stats
.resource-bar.project
{
margin-left
:
100px
;
font-size
:
0.8em
;}
.stats
.resource-bar.project
h3
{
margin-bottom
:
0.5em
;
}
.stats
.resource-bar.project
.bar
div
{
width
:
340px
;
height
:
25px
;
border
:
1px
solid
#000
;
margin-top
:
5px
;
overflow
:
hidden
;
font-size
:
0.8em
}
.projects
.editable
form
textarea
{
width
:
70%
;
height
:
50px
;
max-width
:
70%
;
width
:
270px
;
height
:
120px
;}
...
...
snf-astakos-app/astakos/im/static/im/js/usage.js
View file @
373da652
...
...
@@ -40,7 +40,7 @@ humanize.numberFormat = function(number, decimals, decPoint, thousandsSep) {
};
DO_LOG
=
false
DO_LOG
=
false
;
LOG
=
DO_LOG
?
_
.
bind
(
console
.
log
,
console
)
:
function
()
{};
WARN
=
DO_LOG
?
_
.
bind
(
console
.
warn
,
console
)
:
function
()
{};
...
...
@@ -53,10 +53,13 @@ var default_usage_cls_map = {
function
UsageView
(
settings
)
{
this
.
settings
=
settings
;
this
.
url
=
this
.
settings
.
url
;
this
.
projects_url
=
this
.
settings
.
projects_url
;
this
.
container
=
$
(
this
.
settings
.
container
);
this
.
meta
=
this
.
settings
.
meta
;
this
.
groups
=
this
.
settings
.
groups
;
this
.
projects
=
{};
this
.
el
=
{};
this
.
updating_projects
=
false
;
this
.
usage_cls_map
=
this
.
settings
.
usage_cls_map
||
default_usage_cls_map
;
this
.
initialize
();
}
...
...
@@ -65,11 +68,13 @@ function UsageView(settings) {
_
.
extend
(
UsageView
.
prototype
,
{
tpls
:
{
'
main
'
:
'
<div class="stats clearfix"><ul></ul></div>
'
,
'
quotas
'
:
"
#quotaTpl
"
'
quotas
'
:
"
#quotaTpl
"
,
'
projectQuota
'
:
"
#projectQuotaTpl
"
},
initialize
:
function
()
{
LOG
(
"
Initializing UsageView
"
,
this
.
settings
);
this
.
updateProjects
(
this
.
meta
.
projects_details
);
this
.
initResources
();
// initial usage set ????
...
...
@@ -78,7 +83,6 @@ _.extend(UsageView.prototype, {
this
.
setQuotas
(
this
.
settings
.
quotas
);
}
this
.
initLayout
();
this
.
updateQuotas
();
},
$
:
function
(
selector
)
{
...
...
@@ -101,34 +105,72 @@ _.extend(UsageView.prototype, {
this
.
container
.
append
(
this
.
el
.
main
);
var
ul
=
this
.
container
.
find
(
"
ul
"
);
this
.
el
.
list
=
this
.
render
(
'
quotas
'
,
{
'
resources
'
:
this
.
resources_ordered
'
resources
'
:
this
.
resources_ordered
,
});
ul
.
append
(
this
.
el
.
list
);
ul
.
append
(
this
.
el
.
list
).
hide
();
_
.
each
(
this
.
resources_ordered
,
function
(
resource
)
{
this
.
renderResourceProjects
(
this
.
container
,
resource
);
},
this
);
this
.
updateQuotas
();
ul
.
show
();
},
renderResourceProjects
:
function
(
list
,
resource
)
{
var
resource_el
=
list
.
find
(
"
li[data-resource='
"
+
resource
.
name
+
"
']
"
);
var
projects_el
=
resource_el
.
find
(
"
.projects
"
);
projects_el
.
empty
();
_
.
each
(
resource
.
projects_list
,
function
(
project
)
{
_
.
extend
(
project
,
{
report_desc
:
resource
.
report_desc
});
projects_el
.
append
(
this
.
render
(
'
projectQuota
'
,
project
));
},
this
);
},
initResources
:
function
()
{
var
ordered
=
this
.
meta
.
resources_order
;
var
resources
=
{};
var
resources_ordered
=
[];
var
projects
=
this
.
projects
;
_
.
each
(
this
.
meta
.
resources
,
function
(
group
,
index
)
{
_
.
each
(
group
[
1
],
function
(
resource
,
rindex
)
{
resource
.
resource_name
=
resource
.
name
.
split
(
"
.
"
)[
1
];
resources
[
resource
.
name
]
=
resource
;
}
)
});
}
,
this
);
}
,
this
);
resources_ordered
=
_
.
filter
(
_
.
map
(
ordered
,
function
(
rk
,
index
)
{
rk
.
index
=
index
;
return
resources
[
rk
]
}),
function
(
i
)
{
return
i
});
resources_ordered
=
_
.
filter
(
_
.
map
(
ordered
,
function
(
rk
,
index
)
{
rk
.
index
=
index
;
return
resources
[
rk
]
}),
function
(
i
)
{
return
i
});
this
.
resources
=
resources
;
this
.
resources_ordered
=
resources_ordered
;
LOG
(
"
Resources initialized
"
,
this
.
resources_ordered
,
this
.
resources
);
},
updateProject
:
function
(
uuid
,
details
)
{
LOG
(
"
Update project
"
,
uuid
,
details
);
this
.
projects
[
uuid
]
=
details
;
},
addProject
:
function
(
uuid
,
details
)
{
LOG
(
"
New project
"
,
uuid
,
details
);
this
.
projects
[
uuid
]
=
details
;
},
updateProjects
:
function
(
projects
,
uuids
)
{
this
.
projects
=
{};
_
.
each
(
projects
,
function
(
details
)
{
if
(
this
.
projects
[
details
.
id
])
{
this
.
updateProject
(
details
.
id
,
details
);
}
else
{
this
.
addProject
(
details
.
id
,
details
);
}
},
this
);
LOG
(
"
Projects updated
"
,
this
.
projects
);
},
updateLayout
:
function
()
{
LOG
(
"
Updating layout
"
,
this
.
quotas
);
...
...
@@ -137,25 +179,46 @@ _.extend(UsageView.prototype, {
var
usage
=
self
.
getUsage
(
key
);
if
(
!
usage
)
{
return
}
var
el
=
self
.
$
().
find
(
"
li[data-resource='
"
+
key
+
"
']
"
);
self
.
updateResourceElement
(
el
,
usage
);
self
.
updateResourceElement
(
el
.
find
(
"
.summary.resource-bar
"
),
usage
);
_
.
each
(
self
.
resources
[
key
].
projects_list
,
function
(
project
){
var
project_el
=
el
.
find
(
"
.project-
"
+
project
.
id
);
if
(
project_el
.
length
===
0
)
{
self
.
renderResourceProjects
(
self
.
container
,
self
.
resources
[
key
]);
}
else
{
self
.
updateResourceElement
(
project_el
,
project
.
usage
);
}
});
var
project_ids
=
_
.
keys
(
self
.
project_quotas
);
_
.
each
(
el
.
find
(
"
.resource-bar.project
"
),
function
(
el
)
{
if
(
project_ids
.
indexOf
(
$
(
el
).
data
(
"
project
"
))
==
-
1
)
{
self
.
renderResourceProjects
(
self
.
container
,
self
.
resources
[
key
]);
}
})
})
},
updateResourceElement
:
function
(
el
,
usage
)
{
var
bar_el
=
el
.
find
(
"
.bar span
"
);
var
bar_value
=
el
.
find
(
"
.bar .value
"
);
el
.
find
(
"
.currValue
"
).
text
(
usage
.
curr
);
el
.
find
(
"
.maxValue
"
).
text
(
usage
.
max
);
el
.
find
(
"
.bar span
"
)
.
css
({
width
:
usage
.
perc
+
"
%
"
});
el
.
find
(
"
.bar .value
"
)
.
text
(
usage
.
perc
+
"
%
"
);
bar_el
.
css
({
width
:
usage
.
perc
+
"
%
"
});
bar_value
.
text
(
usage
.
perc
+
"
%
"
);
var
left
=
usage
.
label_left
==
'
auto
'
?
usage
.
label_left
:
usage
.
label_left
+
"
%
"
;
el
.
find
(
"
.bar .value
"
)
.
css
({
left
:
left
});
el
.
find
(
"
.bar .value
"
)
.
css
({
color
:
usage
.
label_color
});
bar_value
.
css
({
left
:
left
});
bar_value
.
css
({
color
:
usage
.
label_color
});
el
.
removeClass
(
"
green yellow red
"
);
el
.
addClass
(
usage
.
cls
);
if
(
el
.
hasClass
(
"
summary
"
))
{
el
.
parent
().
removeClass
(
"
green yellow red
"
);
el
.
parent
().
addClass
(
usage
.
cls
);
};
},
getUsage
:
function
(
resource_name
)
{
var
resource
=
this
.
quotas
[
resource_name
];
getUsage
:
function
(
resource_name
,
quotas
)
{
var
resource
=
quotas
?
quotas
[
resource_name
]
:
this
.
quotas
[
resource_name
];
var
resource_meta
=
this
.
resources
[
resource_name
];
if
(
!
resource_meta
)
{
return
}
var
value
,
limit
,
percentage
;
...
...
@@ -182,19 +245,38 @@ _.extend(UsageView.prototype, {
}
})
var
label_left
=
percentage
>=
30
?
percentage
-
17
:
'
auto
'
;
var
label_left
=
percentage
>=
30
?
percentage
-
17
:
'
auto
'
;
var
label_col
=
label_left
==
'
auto
'
?
'
inherit
'
:
'
#fff
'
;
if
(
label_left
!=
'
auto
'
)
{
label_left
=
label_left
+
"
%
"
}
percentage
=
humanize
.
numberFormat
(
percentage
,
0
);
qdata
=
{
'
curr
'
:
value
,
'
max
'
:
limit
,
'
perc
'
:
percentage
,
'
cls
'
:
cls
,
'
label_left
'
:
label_left
,
'
label_color
'
:
label_col
}
_
.
extend
(
qdata
,
resource
);
return
qdata
},
setQuotas
:
function
(
data
)
{
setQuotas
:
function
(
data
,
update_projects
)
{
LOG
(
"
Set quotas
"
,
data
);
this
.
last_quota_received
=
data
;
var
project_uuids
=
_
.
keys
(
data
);
var
self
=
this
;
this
.
quotas
=
data
;
var
sums
=
{};
_
.
each
(
data
,
function
(
quotas
,
project_uuid
)
{
_
.
each
(
quotas
,
function
(
values
,
qname
)
{
if
(
!
sums
[
qname
])
{
sums
[
qname
]
=
{};
}
var
qitem
=
sums
[
qname
];
_
.
each
(
values
,
function
(
value
,
param
)
{
var
current
=
qitem
[
param
];
qitem
[
param
]
=
current
?
qitem
[
param
]
+
value
:
value
;
},
this
);
});
},
this
);
this
.
project_quotas
=
data
;
this
.
quotas
=
sums
;
_
.
each
(
this
.
quotas
,
function
(
v
,
k
)
{
var
r
=
self
.
resources
[
k
];
var
usage
=
self
.
getUsage
(
k
);
...
...
@@ -204,12 +286,69 @@ _.extend(UsageView.prototype, {
if
(
!
self
.
resources_ordered
[
r
.
index
])
{
return
}
self
.
resources_ordered
[
r
.
index
].
usage
=
usage
;
});
var
active_project_uuids
=
_
.
keys
(
this
.
project_quotas
);
var
project_change
=
false
;
_
.
each
(
this
.
project_quotas
,
function
(
resources
,
uuid
)
{
if
(
project_change
)
{
return
}
_
.
each
(
resources
,
function
(
v
,
k
){
if
(
project_change
)
{
return
}
if
(
!
self
.
resources
[
k
])
{
return
}
if
(
!
self
.
resources
[
k
].
projects
)
{
self
.
resources
[
k
].
projects
=
{};
self
.
resources
[
k
].
projects_list
=
[];
}
var
resource
=
self
.
resources
[
k
];
var
project_usage
=
self
.
getUsage
(
k
,
resources
);
var
resource_projects_list
=
self
.
resources
[
k
].
projects_list
;
if
(
!
self
.
projects
[
uuid
])
{
self
.
getProjects
(
function
(
data
)
{
self
.
updateProjects
(
data
);
self
.
setQuotas
(
self
.
last_quota_received
);
self
.
updateLayout
();
});
project_change
=
true
;
return
;
}
if
(
!
project_usage
)
{
return
;
}
if
(
!
resource
.
projects
[
uuid
])
{
resource
.
projects
[
uuid
]
=
_
.
clone
(
self
.
projects
[
uuid
]);
if
(
self
.
projects
[
uuid
].
base_project
)
{
resource
.
projects
[
uuid
].
name
=
'
User quota
'
}
}
var
resource_project
=
resource
.
projects
[
uuid
];
resource_project
.
usage
=
project_usage
;
if
(
resource_project
.
index
===
undefined
)
{
resource_project
.
index
=
resource_projects_list
.
length
;
resource_projects_list
.
push
(
resource_project
);
}
else
{
resource_projects_list
[
resource_project
.
index
]
=
resource_project
;
}
_
.
each
(
resource
.
projects
,
function
(
project
,
uuid
)
{
if
(
active_project_uuids
.
indexOf
(
uuid
)
==
-
1
)
{
var
index
=
resource
.
projects
[
uuid
].
index
;
delete
resource
.
projects
[
uuid
];
delete
resource
.
projects_list
[
index
];
}
});
});
});
},
_ajaxOptions
:
function
()
{
_ajaxOptions
:
function
(
url
)
{
var
token
=
$
.
cookie
(
this
.
settings
.
cookie_name
).
split
(
"
|
"
)[
1
];
return
{
'
url
'
:
this
.
url
,
'
url
'
:
url
||
this
.
url
,
'
headers
'
:
{
'
X-Auth-Token
'
:
token
},
...
...
@@ -220,12 +359,20 @@ _.extend(UsageView.prototype, {
LOG
(
"
Updating quotas
"
);
var
self
=
this
;
this
.
getQuotas
(
function
(
data
){
self
.
setQuotas
(
data
.
system
);
self
.
setQuotas
(
data
,
true
);
self
.
updateLayout
();
})
},
getProjects
:
function
(
callback
)
{
var
options
=
this
.
_ajaxOptions
(
this
.
projects_url
);
options
.
success
=
callback
;
LOG
(
"
Calling projects API
"
,
options
);
$
.
ajax
(
options
);
},
getQuotas
:
function
(
callback
)
{
if
(
this
.
updating_projects
)
{
return
}
var
options
=
this
.
_ajaxOptions
();
options
.
success
=
callback
;
LOG
(
"
Calling quotas API
"
,
options
);
...
...
snf-astakos-app/astakos/im/templates/im/resource_usage.html
View file @
373da652
...
...
@@ -10,6 +10,29 @@
{% block page.title %}Usage{% endblock %}
{% block page.body %}
<script
id=
"projectQuotaTpl"
type=
"text/template"
>
{
%
verbatim
%
}
<
div
class
=
"
resource-bar project clearfix project-{{id}} {{ usage.cls }}
"
data
-
project
=
"
{{ id }}
"
>
<
div
class
=
"
info
"
data
-
currvalue
=
""
data
-
maxvalue
=
""
>
<
h3
>
{{
name
}}
<
/h3
>
<
p
>
<
span
class
=
"
currValue
"
>
{{
usage
.
curr
}}
<
/span> out of
<
span
class
=
"
maxValue
"
>
{{
usage
.
max
}}
<
/span> {{ report_desc }
}
<
/p
>
<
/div
>
<
div
class
=
"
bar
"
>
<
div
>
<
span
style
=
"
width:{{ usage.perc }}%
"
><
/span
>
<
em
class
=
"
value
"
style
=
"
left: {{ usage.label_left }}; color: {{ usage.label_color }}
"
>
{{
usage
.
perc
}}
%
<
/em
>
<
/div
>
<
/div
>
<
/div
>
{
%
endverbatim
%
}
</script>
<script
id=
"quotaTpl"
type=
"text/template"
>
{
%
verbatim
%
}
...
...
@@ -17,22 +40,26 @@
<
li
class
=
"
clearfix {{ resource_name }} {{ usage.cls }}
"
data
-
resource
=
"
{{ name }}
"
>
<
div
class
=
"
img-wrap
"
>&
nbsp
;
<
/div
>
<
div
class
=
"
info
"
data
-
currvalue
=
""
data
-
maxvalue
=
""
>
<
h3
>
{{
report_desc
}}
<
/h3
>
<
p
>
<
span
class
=
"
currValue
"
>
{{
usage
.
curr
}}
<
/span> out of
<
span
class
=
"
maxValue
"
>
{{
usage
.
max
}}
<
/span> {{ report_desc }
}
<
/p
>
<
div
class
=
"
summary resource-bar clearfix {{ usage.cls }}
"
>
<
div
class
=
"
info
"
data
-
currvalue
=
""
data
-
maxvalue
=
""
>
<
h3
>
{{
report_desc
}}
<
/h3
>
<
p
>
<
span
class
=
"
currValue
"
>
{{
usage
.
curr
}}
<
/span> out of
<
span
class
=
"
maxValue
"
>
{{
usage
.
max
}}
<
/span> {{ report_desc }
}
<
/p
>
<!--<
span
class
=
"
resource-extend
"
>
details
<
/span>--
>
<
/div
>
<
div
class
=
"
bar
"
>
<
div
>
<
span
style
=
"
width:{{ usage.perc }}%
"
><
/span
>
<
em
class
=
"
value
"
style
=
"
left: {{ usage.label_left }}; color: {{ usage.label_color }}
"
>
{{
usage
.
perc
}}
%
<
/em
>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
bar
"
>
<
div
>
<
span
style
=
"
width:{{ usage.perc }}%
"
><
/span
>
<
em
class
=
"
value
"
style
=
"
left: {{ usage.label_left }}%; color: {{ usage.label_color }}
"
>
{{
usage
.
perc
}}
%
<
/em
>
<
/div
>
<
div
class
=
"
projects
"
>
<
/div
>
<
/li
>
{{
/
resources
}}
...
...
@@ -48,13 +75,15 @@
$
(
document
).
ready
(
function
(){
var
usageView
=
new
UsageView
({
'
url
'
:
'
{% url astakos-api-quotas %}
'
,
'
projects_url
'
:
'
{% url astakos.api.projects.projects %}
'
,
'
cookie_name
'
:
'
{{ token_cookie_name|safe }}
'
,
'
dataType
'
:
'
json
'
,
'
container
'
:
'
#quota-container
'
,
'
quotas
'
:
{{
current_usage
|
safe
}},
'
quotas
'
:
{{
user_quotas
|
safe
}},
'
meta
'
:
{
'
resources
'
:
{{
resource_catalog
|
safe
}},
'
groups
'
:
{{
resource_groups
|
safe
}},
'
projects_details
'
:
{{
projects_details
|
safe
}},
'
resources_order
'
:
{{
resources_order
|
safe
}}
}
});
...
...
snf-astakos-app/astakos/im/views/im.py
View file @
373da652
...
...
@@ -74,6 +74,8 @@ from astakos.im import quotas
from
astakos.im.views.util
import
render_response
,
_resources_catalog
from
astakos.im.views.decorators
import
cookie_fix
,
signed_terms_required
,
\
required_auth_methods_assigned
,
valid_astakos_user_required
,
login_required
from
astakos.api
import
projects
as
projects_api
from
astakos.api.util
import
_dthandler
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -840,8 +842,11 @@ def resource_usage(request):
resources_meta
=
presentation
.
RESOURCES
current_usage
=
quotas
.
get_user_quotas
(
request
.
user
)
current_usage
=
json
.
dumps
(
current_usage
[
'system'
])
user_memberships
=
request
.
user
.
projectmembership_set
.
actually_accepted
()
sources
=
[
quotas
.
project_ref
(
m
.
project
.
uuid
)
for
m
in
user_memberships
]
user_quotas
=
quotas
.
get_user_quotas
(
request
.
user
,
sources
=
sources
)
projects
=
[
m
.
project
for
m
in
user_memberships
]
user_projects
=
projects_api
.
get_projects_details
(
projects
)
resource_catalog
,
resource_groups
=
_resources_catalog
()
if
resource_catalog
is
False
:
# on fail resource_groups contains the result object
...
...
@@ -852,16 +857,20 @@ def resource_usage(request):
resource_catalog
=
json
.
dumps
(
resource_catalog
)
resource_groups
=
json
.
dumps
(
resource_groups
)
resources_order
=
json
.
dumps
(
resources_meta
.
get
(
'resources_order'
))
projects_details
=
json
.
dumps
(
user_projects
,
default
=
_dthandler
)
user_quotas
=
json
.
dumps
(
user_quotas
)
interval
=
settings
.
USAGE_UPDATE_INTERVAL
return
render_response
(
'im/resource_usage.html'
,
context_instance
=
get_context
(
request
),
resource_catalog
=
resource_catalog
,
resource_groups
=
resource_groups
,
resources_order
=
resources_order
,
current_usage
=
current_usage
,
projects_details
=
projects_details
,
user_quotas
=
user_quotas
,
token_cookie_name
=
settings
.
COOKIE_NAME
,
usage_update_interval
=
settings
.
USAGE_UPDATE_INTERVAL
)
usage_update_interval
=
interval
)
# TODO: action only on POST and user should confirm the removal
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment