##// END OF EJS Templates
integrations: expose EE integrations as dummy objects to show...
marcink -
r2138:cfe7604e default
parent child Browse files
Show More
@@ -1,62 +1,68 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode.integrations.registry import IntegrationTypeRegistry
24 from rhodecode.integrations.types import webhook, slack, hipchat, email
25
24 from rhodecode.integrations.types import webhook, slack, hipchat, email, base
26 25 log = logging.getLogger(__name__)
27 26
28 27
29 28 # TODO: dan: This is currently global until we figure out what to do about
30 29 # VCS's not having a pyramid context - move it to pyramid app configuration
31 30 # includeme level later to allow per instance integration setup
32 31 integration_type_registry = IntegrationTypeRegistry()
33 32
34 33 integration_type_registry.register_integration_type(
35 34 webhook.WebhookIntegrationType)
36 35 integration_type_registry.register_integration_type(
37 36 slack.SlackIntegrationType)
38 37 integration_type_registry.register_integration_type(
39 38 hipchat.HipchatIntegrationType)
40 39 integration_type_registry.register_integration_type(
41 40 email.EmailIntegrationType)
42 41
43 42
43 # dummy EE integration to show users what we have in EE edition
44 integration_type_registry.register_integration_type(
45 base.EEIntegration('Jira Issues integration', 'jira'))
46 integration_type_registry.register_integration_type(
47 base.EEIntegration('Redmine Tracker integration', 'redmine'))
48
49
44 50 def integrations_event_handler(event):
45 51 """
46 52 Takes an event and passes it to all enabled integrations
47 53 """
48 54 from rhodecode.model.integration import IntegrationModel
49 55
50 56 integration_model = IntegrationModel()
51 57 integrations = integration_model.get_for_event(event)
52 58 for integration in integrations:
53 59 try:
54 60 integration_model.send_event(integration, event)
55 61 except Exception:
56 62 log.exception(
57 63 'failure occurred when sending event %s to integration %s' % (
58 64 event, integration))
59 65
60 66
61 67 def includeme(config):
62 68 config.include('rhodecode.integrations.routes')
@@ -1,37 +1,38 b''
1 1 # -*- coding: utf-8 -*-
2 2 # Copyright (C) 2012-2017 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 import collections
21 22
22 23 log = logging.getLogger(__name__)
23 24
24 25
25 class IntegrationTypeRegistry(dict):
26 class IntegrationTypeRegistry(collections.OrderedDict):
26 27 """
27 28 Registry Class to hold IntegrationTypes
28 29 """
29 30 def register_integration_type(self, IntegrationType):
30 31 key = IntegrationType.key
31 32 if key in self:
32 33 log.warning(
33 34 'Overriding existing integration type %s (%s) with %s' % (
34 35 self[key], key, IntegrationType))
35 36
36 37 self[key] = IntegrationType
37 38
@@ -1,264 +1,269 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.model.db import Integration
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.integrations import integration_type_registry
27 27
28 28
29 29 def route_path(name, **kwargs):
30 30 return {
31 31 'home': '/',
32 32 }[name].format(**kwargs)
33 33
34 34
35 35 @pytest.mark.usefixtures('app', 'autologin_user')
36 36 class TestIntegrationsView(object):
37 37 pass
38 38
39 39
40 40 class TestGlobalIntegrationsView(TestIntegrationsView):
41 41 def test_index_no_integrations(self):
42 42 url = ADMIN_PREFIX + '/integrations'
43 43 response = self.app.get(url)
44 44
45 45 assert response.status_code == 200
46 assert 'exist yet' in response.body
46 response.mustcontain('exist yet')
47 47
48 48 def test_index_with_integrations(self, global_integration_stub):
49 49 url = ADMIN_PREFIX + '/integrations'
50 50 response = self.app.get(url)
51 51
52 52 assert response.status_code == 200
53 assert 'exist yet' not in response.body
54 assert global_integration_stub.name in response.body
53 response.mustcontain(no=['exist yet'])
54 response.mustcontain(global_integration_stub.name)
55 55
56 56 @pytest.mark.parametrize(
57 57 'IntegrationType', integration_type_registry.values())
58 58 def test_new_integration_page(self, IntegrationType):
59 59 url = ADMIN_PREFIX + '/integrations/new'
60 60
61 61 response = self.app.get(url, status=200)
62
63 url = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 integration=IntegrationType.key)
65 assert url in response.body
62 if not IntegrationType.is_dummy:
63 url = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 integration=IntegrationType.key)
65 response.mustcontain(url)
66 66
67 67 @pytest.mark.parametrize(
68 68 'IntegrationType', integration_type_registry.values())
69 69 def test_get_create_integration_page(self, IntegrationType):
70 70 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
71 71 integration_key=IntegrationType.key)
72
73 response = self.app.get(url, status=200)
74
75 assert IntegrationType.display_name in response.body
72 if IntegrationType.is_dummy:
73 self.app.get(url, status=404)
74 else:
75 response = self.app.get(url, status=200)
76 response.mustcontain(IntegrationType.display_name)
76 77
77 78 def test_post_integration_page(self, StubIntegrationType, csrf_token,
78 79 test_repo_group, backend_random):
79 80 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
80 81 integration_key=StubIntegrationType.key)
81 82
82 _post_integration_test_helper(self.app, url, csrf_token, admin_view=True,
83 _post_integration_test_helper(
84 self.app, url, csrf_token, admin_view=True,
83 85 repo=backend_random.repo, repo_group=test_repo_group)
84 86
85 87
86 88 class TestRepoIntegrationsView(TestIntegrationsView):
87 89 def test_index_no_integrations(self, backend_random):
88 90 url = '/{repo_name}/settings/integrations'.format(
89 91 repo_name=backend_random.repo.repo_name)
90 92 response = self.app.get(url)
91 93
92 94 assert response.status_code == 200
93 assert 'exist yet' in response.body
95 response.mustcontain('exist yet')
94 96
95 97 def test_index_with_integrations(self, repo_integration_stub):
96 98 url = '/{repo_name}/settings/integrations'.format(
97 99 repo_name=repo_integration_stub.repo.repo_name)
98 100 stub_name = repo_integration_stub.name
99 101
100 102 response = self.app.get(url)
101 103
102 104 assert response.status_code == 200
103 assert stub_name in response.body
104 assert 'exist yet' not in response.body
105 response.mustcontain(stub_name)
106 response.mustcontain(no=['exist yet'])
105 107
106 108 @pytest.mark.parametrize(
107 109 'IntegrationType', integration_type_registry.values())
108 110 def test_new_integration_page(self, backend_random, IntegrationType):
109 111 repo_name = backend_random.repo.repo_name
110 112 url = '/{repo_name}/settings/integrations/new'.format(
111 113 repo_name=repo_name)
112 114
113 115 response = self.app.get(url, status=200)
114 116
115 117 url = '/{repo_name}/settings/integrations/{integration}/new'.format(
116 118 repo_name=repo_name,
117 119 integration=IntegrationType.key)
118
119 assert url in response.body
120 if not IntegrationType.is_dummy:
121 response.mustcontain(url)
120 122
121 123 @pytest.mark.parametrize(
122 124 'IntegrationType', integration_type_registry.values())
123 125 def test_get_create_integration_page(self, backend_random, IntegrationType):
124 126 repo_name = backend_random.repo.repo_name
125 127 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
126 128 repo_name=repo_name, integration_key=IntegrationType.key)
127
128 response = self.app.get(url, status=200)
129
130 assert IntegrationType.display_name in response.body
129 if IntegrationType.is_dummy:
130 self.app.get(url, status=404)
131 else:
132 response = self.app.get(url, status=200)
133 response.mustcontain(IntegrationType.display_name)
131 134
132 135 def test_post_integration_page(self, backend_random, test_repo_group,
133 136 StubIntegrationType, csrf_token):
134 137 repo_name = backend_random.repo.repo_name
135 138 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
136 139 repo_name=repo_name, integration_key=StubIntegrationType.key)
137 140
138 141 _post_integration_test_helper(
139 142 self.app, url, csrf_token, admin_view=False,
140 143 repo=backend_random.repo, repo_group=test_repo_group)
141 144
142 145
143 146 class TestRepoGroupIntegrationsView(TestIntegrationsView):
144 147 def test_index_no_integrations(self, test_repo_group):
145 148 url = '/{repo_group_name}/settings/integrations'.format(
146 149 repo_group_name=test_repo_group.group_name)
147 150 response = self.app.get(url)
148 151
149 152 assert response.status_code == 200
150 assert 'exist yet' in response.body
153 response.mustcontain('exist yet')
151 154
152 155 def test_index_with_integrations(
153 156 self, test_repo_group, repogroup_integration_stub):
154 157
155 158 url = '/{repo_group_name}/settings/integrations'.format(
156 159 repo_group_name=test_repo_group.group_name)
157 160
158 161 stub_name = repogroup_integration_stub.name
159 162 response = self.app.get(url)
160 163
161 164 assert response.status_code == 200
162 assert 'exist yet' not in response.body
163 assert stub_name in response.body
165 response.mustcontain(no=['exist yet'])
166 response.mustcontain(stub_name)
164 167
165 168 def test_new_integration_page(self, test_repo_group):
166 169 repo_group_name = test_repo_group.group_name
167 170 url = '/{repo_group_name}/settings/integrations/new'.format(
168 171 repo_group_name=test_repo_group.group_name)
169 172
170 173 response = self.app.get(url)
171 174
172 175 assert response.status_code == 200
173 176
174 for integration_key in integration_type_registry:
175 nurl = ('/{repo_group_name}/settings/integrations/{integration}/new').format(
177 for integration_key, integration_obj in integration_type_registry.items():
178 if not integration_obj.is_dummy:
179 nurl = (
180 '/{repo_group_name}/settings/integrations/{integration}/new').format(
176 181 repo_group_name=repo_group_name,
177 182 integration=integration_key)
178
179 assert nurl in response.body
183 response.mustcontain(nurl)
180 184
181 185 @pytest.mark.parametrize(
182 186 'IntegrationType', integration_type_registry.values())
183 187 def test_get_create_integration_page(
184 188 self, test_repo_group, IntegrationType):
185 189
186 190 repo_group_name = test_repo_group.group_name
187 191 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
188 192 ).format(repo_group_name=repo_group_name,
189 193 integration_key=IntegrationType.key)
190 194
191 response = self.app.get(url)
192
193 assert response.status_code == 200
194 assert IntegrationType.display_name in response.body
195 if not IntegrationType.is_dummy:
196 response = self.app.get(url, status=200)
197 response.mustcontain(IntegrationType.display_name)
198 else:
199 self.app.get(url, status=404)
195 200
196 201 def test_post_integration_page(self, test_repo_group, backend_random,
197 202 StubIntegrationType, csrf_token):
198 203
199 204 repo_group_name = test_repo_group.group_name
200 205 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
201 206 ).format(repo_group_name=repo_group_name,
202 207 integration_key=StubIntegrationType.key)
203 208
204 209 _post_integration_test_helper(
205 210 self.app, url, csrf_token, admin_view=False,
206 211 repo=backend_random.repo, repo_group=test_repo_group)
207 212
208 213
209 214 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
210 215 admin_view):
211 216 """
212 217 Posts form data to create integration at the url given then deletes it and
213 218 checks if the redirect url is correct.
214 219 """
215 220 repo_name = repo.repo_name
216 221 repo_group_name = repo_group.group_name
217 222 app.post(url, params={}, status=403) # missing csrf check
218 223 response = app.post(url, params={'csrf_token': csrf_token})
219 224 assert response.status_code == 200
220 assert 'Errors exist' in response.body
225 response.mustcontain('Errors exist')
221 226
222 227 scopes_destinations = [
223 228 ('global',
224 229 ADMIN_PREFIX + '/integrations'),
225 230 ('root-repos',
226 231 ADMIN_PREFIX + '/integrations'),
227 232 ('repo:%s' % repo_name,
228 233 '/%s/settings/integrations' % repo_name),
229 234 ('repogroup:%s' % repo_group_name,
230 235 '/%s/settings/integrations' % repo_group_name),
231 236 ('repogroup-recursive:%s' % repo_group_name,
232 237 '/%s/settings/integrations' % repo_group_name),
233 238 ]
234 239
235 240 for scope, destination in scopes_destinations:
236 241 if admin_view:
237 242 destination = ADMIN_PREFIX + '/integrations'
238 243
239 244 form_data = [
240 245 ('csrf_token', csrf_token),
241 246 ('__start__', 'options:mapping'),
242 247 ('name', 'test integration'),
243 248 ('scope', scope),
244 249 ('enabled', 'true'),
245 250 ('__end__', 'options:mapping'),
246 251 ('__start__', 'settings:mapping'),
247 252 ('test_int_field', '34'),
248 253 ('test_string_field', ''), # empty value on purpose as it's required
249 254 ('__end__', 'settings:mapping'),
250 255 ]
251 256 errors_response = app.post(url, form_data)
252 257 assert 'Errors exist' in errors_response.body
253 258
254 259 form_data[-2] = ('test_string_field', 'data!')
255 260 assert Session().query(Integration).count() == 0
256 261 created_response = app.post(url, form_data)
257 262 assert Session().query(Integration).count() == 1
258 263
259 264 delete_response = app.post(
260 265 created_response.location,
261 266 params={'csrf_token': csrf_token, 'delete': 'delete'})
262 267
263 268 assert Session().query(Integration).count() == 0
264 269 assert delete_response.location.endswith(destination)
@@ -1,101 +1,110 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 from rhodecode.translation import _
23 23
24 24
25 25 class IntegrationTypeBase(object):
26 26 """ Base class for IntegrationType plugins """
27
27 is_dummy = False
28 28 description = ''
29 29 icon = '''
30 30 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
31 31 <svg
32 32 xmlns:dc="http://purl.org/dc/elements/1.1/"
33 33 xmlns:cc="http://creativecommons.org/ns#"
34 34 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
35 35 xmlns:svg="http://www.w3.org/2000/svg"
36 36 xmlns="http://www.w3.org/2000/svg"
37 37 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
38 38 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
39 39 viewBox="0 -256 1792 1792"
40 40 id="svg3025"
41 41 version="1.1"
42 42 inkscape:version="0.48.3.1 r9886"
43 43 width="100%"
44 44 height="100%"
45 45 sodipodi:docname="cog_font_awesome.svg">
46 46 <metadata
47 47 id="metadata3035">
48 48 <rdf:RDF>
49 49 <cc:Work
50 50 rdf:about="">
51 51 <dc:format>image/svg+xml</dc:format>
52 52 <dc:type
53 53 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
54 54 </cc:Work>
55 55 </rdf:RDF>
56 56 </metadata>
57 57 <defs
58 58 id="defs3033" />
59 59 <sodipodi:namedview
60 60 pagecolor="#ffffff"
61 61 bordercolor="#666666"
62 62 borderopacity="1"
63 63 objecttolerance="10"
64 64 gridtolerance="10"
65 65 guidetolerance="10"
66 66 inkscape:pageopacity="0"
67 67 inkscape:pageshadow="2"
68 68 inkscape:window-width="640"
69 69 inkscape:window-height="480"
70 70 id="namedview3031"
71 71 showgrid="false"
72 72 inkscape:zoom="0.13169643"
73 73 inkscape:cx="896"
74 74 inkscape:cy="896"
75 75 inkscape:window-x="0"
76 76 inkscape:window-y="25"
77 77 inkscape:window-maximized="0"
78 78 inkscape:current-layer="svg3025" />
79 79 <g
80 80 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
81 81 id="g3027">
82 82 <path
83 83 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
84 84 id="path3029"
85 85 inkscape:connector-curvature="0"
86 86 style="fill:currentColor" />
87 87 </g>
88 88 </svg>
89 89 '''
90 90
91 91 def __init__(self, settings):
92 92 """
93 93 :param settings: dict of settings to be used for the integration
94 94 """
95 95 self.settings = settings
96 96
97 97 def settings_schema(self):
98 98 """
99 99 A colander schema of settings for the integration type
100 100 """
101 101 return colander.Schema()
102
103
104 class EEIntegration(IntegrationTypeBase):
105 description = 'Integration available in RhodeCode EE edition.'
106 is_dummy = True
107
108 def __init__(self, name, key, settings=None):
109 self.display_name = name
110 self.key = key
@@ -1,454 +1,459 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import deform
22 22 import logging
23 23 import peppercorn
24 24 import webhelpers.paginate
25 25
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
27 27
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode.integrations import integration_type_registry
30 30 from rhodecode.apps.admin.navigation import navigation_list
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, CSRFRequired, HasPermissionAnyDecorator,
33 33 HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
34 34 from rhodecode.lib.utils2 import safe_int
35 35 from rhodecode.lib.helpers import Page
36 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.integration import IntegrationModel
39 39 from rhodecode.model.validation_schema.schemas.integration_schema import (
40 40 make_integration_schema, IntegrationScopeType)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class IntegrationSettingsViewBase(BaseAppView):
46 46 """
47 47 Base Integration settings view used by both repo / global settings
48 48 """
49 49
50 50 def __init__(self, context, request):
51 51 super(IntegrationSettingsViewBase, self).__init__(context, request)
52 52 self._load_view_context()
53 53
54 54 def _load_view_context(self):
55 55 """
56 56 This avoids boilerplate for repo/global+list/edit+views/templates
57 57 by doing all possible contexts at the same time however it should
58 58 be split up into separate functions once more "contexts" exist
59 59 """
60 60
61 61 self.IntegrationType = None
62 62 self.repo = None
63 63 self.repo_group = None
64 64 self.integration = None
65 65 self.integrations = {}
66 66
67 67 request = self.request
68 68
69 69 if 'repo_name' in request.matchdict: # in repo settings context
70 70 repo_name = request.matchdict['repo_name']
71 71 self.repo = Repository.get_by_repo_name(repo_name)
72 72
73 73 if 'repo_group_name' in request.matchdict: # in group settings context
74 74 repo_group_name = request.matchdict['repo_group_name']
75 75 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
76 76
77 77 if 'integration' in request.matchdict: # integration type context
78 78 integration_type = request.matchdict['integration']
79 if integration_type not in integration_type_registry:
80 raise HTTPNotFound()
81
79 82 self.IntegrationType = integration_type_registry[integration_type]
83 if self.IntegrationType.is_dummy:
84 raise HTTPNotFound()
80 85
81 86 if 'integration_id' in request.matchdict: # single integration context
82 87 integration_id = request.matchdict['integration_id']
83 88 self.integration = Integration.get(integration_id)
84 89
85 90 # extra perms check just in case
86 91 if not self._has_perms_for_integration(self.integration):
87 92 raise HTTPForbidden()
88 93
89 94 self.settings = self.integration and self.integration.settings or {}
90 95 self.admin_view = not (self.repo or self.repo_group)
91 96
92 97 def _has_perms_for_integration(self, integration):
93 98 perms = self.request.user.permissions
94 99
95 100 if 'hg.admin' in perms['global']:
96 101 return True
97 102
98 103 if integration.repo:
99 104 return perms['repositories'].get(
100 105 integration.repo.repo_name) == 'repository.admin'
101 106
102 107 if integration.repo_group:
103 108 return perms['repositories_groups'].get(
104 109 integration.repo_group.group_name) == 'group.admin'
105 110
106 111 return False
107 112
108 113 def _get_local_tmpl_context(self, include_app_defaults=False):
109 114 _ = self.request.translate
110 115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
111 116 include_app_defaults=include_app_defaults)
112 117
113 118 c.active = 'integrations'
114 119
115 120 return c
116 121
117 122 def _form_schema(self):
118 123 schema = make_integration_schema(IntegrationType=self.IntegrationType,
119 124 settings=self.settings)
120 125
121 126 # returns a clone, important if mutating the schema later
122 127 return schema.bind(
123 128 permissions=self.request.user.permissions,
124 129 no_scope=not self.admin_view)
125 130
126 131 def _form_defaults(self):
127 132 _ = self.request.translate
128 133 defaults = {}
129 134
130 135 if self.integration:
131 136 defaults['settings'] = self.integration.settings or {}
132 137 defaults['options'] = {
133 138 'name': self.integration.name,
134 139 'enabled': self.integration.enabled,
135 140 'scope': {
136 141 'repo': self.integration.repo,
137 142 'repo_group': self.integration.repo_group,
138 143 'child_repos_only': self.integration.child_repos_only,
139 144 },
140 145 }
141 146 else:
142 147 if self.repo:
143 148 scope = _('{repo_name} repository').format(
144 149 repo_name=self.repo.repo_name)
145 150 elif self.repo_group:
146 151 scope = _('{repo_group_name} repo group').format(
147 152 repo_group_name=self.repo_group.group_name)
148 153 else:
149 154 scope = _('Global')
150 155
151 156 defaults['options'] = {
152 157 'enabled': True,
153 158 'name': _('{name} integration').format(
154 159 name=self.IntegrationType.display_name),
155 160 }
156 161 defaults['options']['scope'] = {
157 162 'repo': self.repo,
158 163 'repo_group': self.repo_group,
159 164 }
160 165
161 166 return defaults
162 167
163 168 def _delete_integration(self, integration):
164 169 _ = self.request.translate
165 170 Session().delete(integration)
166 171 Session().commit()
167 172 self.request.session.flash(
168 173 _('Integration {integration_name} deleted successfully.').format(
169 174 integration_name=integration.name),
170 175 queue='success')
171 176
172 177 if self.repo:
173 178 redirect_to = self.request.route_path(
174 179 'repo_integrations_home', repo_name=self.repo.repo_name)
175 180 elif self.repo_group:
176 181 redirect_to = self.request.route_path(
177 182 'repo_group_integrations_home',
178 183 repo_group_name=self.repo_group.group_name)
179 184 else:
180 185 redirect_to = self.request.route_path('global_integrations_home')
181 186 raise HTTPFound(redirect_to)
182 187
183 188 def _integration_list(self):
184 189 """ List integrations """
185 190
186 191 c = self.load_default_context()
187 192 if self.repo:
188 193 scope = self.repo
189 194 elif self.repo_group:
190 195 scope = self.repo_group
191 196 else:
192 197 scope = 'all'
193 198
194 199 integrations = []
195 200
196 201 for IntType, integration in IntegrationModel().get_integrations(
197 202 scope=scope, IntegrationType=self.IntegrationType):
198 203
199 204 # extra permissions check *just in case*
200 205 if not self._has_perms_for_integration(integration):
201 206 continue
202 207
203 208 integrations.append((IntType, integration))
204 209
205 210 sort_arg = self.request.GET.get('sort', 'name:asc')
206 211 if ':' in sort_arg:
207 212 sort_field, sort_dir = sort_arg.split(':')
208 213 else:
209 214 sort_field = sort_arg, 'asc'
210 215
211 216 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
212 217
213 218 integrations.sort(
214 219 key=lambda x: getattr(x[1], sort_field),
215 220 reverse=(sort_dir == 'desc'))
216 221
217 222 page_url = webhelpers.paginate.PageURL(
218 223 self.request.path, self.request.GET)
219 224 page = safe_int(self.request.GET.get('page', 1), 1)
220 225
221 226 integrations = Page(
222 227 integrations, page=page, items_per_page=10, url=page_url)
223 228
224 229 c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc'
225 230
226 231 c.current_IntegrationType = self.IntegrationType
227 232 c.integrations_list = integrations
228 233 c.available_integrations = integration_type_registry
229 234
230 235 return self._get_template_context(c)
231 236
232 237 def _settings_get(self, defaults=None, form=None):
233 238 """
234 239 View that displays the integration settings as a form.
235 240 """
236 241 c = self.load_default_context()
237 242
238 243 defaults = defaults or self._form_defaults()
239 244 schema = self._form_schema()
240 245
241 246 if self.integration:
242 247 buttons = ('submit', 'delete')
243 248 else:
244 249 buttons = ('submit',)
245 250
246 251 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
247 252
248 253 c.form = form
249 254 c.current_IntegrationType = self.IntegrationType
250 255 c.integration = self.integration
251 256
252 257 return self._get_template_context(c)
253 258
254 259 def _settings_post(self):
255 260 """
256 261 View that validates and stores the integration settings.
257 262 """
258 263 _ = self.request.translate
259 264
260 265 controls = self.request.POST.items()
261 266 pstruct = peppercorn.parse(controls)
262 267
263 268 if self.integration and pstruct.get('delete'):
264 269 return self._delete_integration(self.integration)
265 270
266 271 schema = self._form_schema()
267 272
268 273 skip_settings_validation = False
269 274 if self.integration and 'enabled' not in pstruct.get('options', {}):
270 275 skip_settings_validation = True
271 276 schema['settings'].validator = None
272 277 for field in schema['settings'].children:
273 278 field.validator = None
274 279 field.missing = ''
275 280
276 281 if self.integration:
277 282 buttons = ('submit', 'delete')
278 283 else:
279 284 buttons = ('submit',)
280 285
281 286 form = deform.Form(schema, buttons=buttons)
282 287
283 288 if not self.admin_view:
284 289 # scope is read only field in these cases, and has to be added
285 290 options = pstruct.setdefault('options', {})
286 291 if 'scope' not in options:
287 292 options['scope'] = IntegrationScopeType().serialize(None, {
288 293 'repo': self.repo,
289 294 'repo_group': self.repo_group,
290 295 })
291 296
292 297 try:
293 298 valid_data = form.validate_pstruct(pstruct)
294 299 except deform.ValidationFailure as e:
295 300 self.request.session.flash(
296 301 _('Errors exist when saving integration settings. '
297 302 'Please check the form inputs.'),
298 303 queue='error')
299 304 return self._settings_get(form=e)
300 305
301 306 if not self.integration:
302 307 self.integration = Integration()
303 308 self.integration.integration_type = self.IntegrationType.key
304 309 Session().add(self.integration)
305 310
306 311 scope = valid_data['options']['scope']
307 312
308 313 IntegrationModel().update_integration(self.integration,
309 314 name=valid_data['options']['name'],
310 315 enabled=valid_data['options']['enabled'],
311 316 settings=valid_data['settings'],
312 317 repo=scope['repo'],
313 318 repo_group=scope['repo_group'],
314 319 child_repos_only=scope['child_repos_only'],
315 320 )
316 321
317 322 self.integration.settings = valid_data['settings']
318 323 Session().commit()
319 324 # Display success message and redirect.
320 325 self.request.session.flash(
321 326 _('Integration {integration_name} updated successfully.').format(
322 327 integration_name=self.IntegrationType.display_name),
323 328 queue='success')
324 329
325 330 # if integration scope changes, we must redirect to the right place
326 331 # keeping in mind if the original view was for /repo/ or /_admin/
327 332 admin_view = not (self.repo or self.repo_group)
328 333
329 334 if self.integration.repo and not admin_view:
330 335 redirect_to = self.request.route_path(
331 336 'repo_integrations_edit',
332 337 repo_name=self.integration.repo.repo_name,
333 338 integration=self.integration.integration_type,
334 339 integration_id=self.integration.integration_id)
335 340 elif self.integration.repo_group and not admin_view:
336 341 redirect_to = self.request.route_path(
337 342 'repo_group_integrations_edit',
338 343 repo_group_name=self.integration.repo_group.group_name,
339 344 integration=self.integration.integration_type,
340 345 integration_id=self.integration.integration_id)
341 346 else:
342 347 redirect_to = self.request.route_path(
343 348 'global_integrations_edit',
344 349 integration=self.integration.integration_type,
345 350 integration_id=self.integration.integration_id)
346 351
347 352 return HTTPFound(redirect_to)
348 353
349 354 def _new_integration(self):
350 355 c = self.load_default_context()
351 356 c.available_integrations = integration_type_registry
352 357 return self._get_template_context(c)
353 358
354 359 def load_default_context(self):
355 360 raise NotImplementedError()
356 361
357 362
358 363 class GlobalIntegrationsView(IntegrationSettingsViewBase):
359 364 def load_default_context(self):
360 365 c = self._get_local_tmpl_context()
361 366 c.repo = self.repo
362 367 c.repo_group = self.repo_group
363 368 c.navlist = navigation_list(self.request)
364 369 self._register_global_c(c)
365 370 return c
366 371
367 372 @LoginRequired()
368 373 @HasPermissionAnyDecorator('hg.admin')
369 374 def integration_list(self):
370 375 return self._integration_list()
371 376
372 377 @LoginRequired()
373 378 @HasPermissionAnyDecorator('hg.admin')
374 379 def settings_get(self):
375 380 return self._settings_get()
376 381
377 382 @LoginRequired()
378 383 @HasPermissionAnyDecorator('hg.admin')
379 384 @CSRFRequired()
380 385 def settings_post(self):
381 386 return self._settings_post()
382 387
383 388 @LoginRequired()
384 389 @HasPermissionAnyDecorator('hg.admin')
385 390 def new_integration(self):
386 391 return self._new_integration()
387 392
388 393
389 394 class RepoIntegrationsView(IntegrationSettingsViewBase):
390 395 def load_default_context(self):
391 396 c = self._get_local_tmpl_context()
392 397
393 398 c.repo = self.repo
394 399 c.repo_group = self.repo_group
395 400
396 401 self.db_repo = self.repo
397 402 c.rhodecode_db_repo = self.repo
398 403 c.repo_name = self.db_repo.repo_name
399 404 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
400 405
401 406 self._register_global_c(c)
402 407 return c
403 408
404 409 @LoginRequired()
405 410 @HasRepoPermissionAnyDecorator('repository.admin')
406 411 def integration_list(self):
407 412 return self._integration_list()
408 413
409 414 @LoginRequired()
410 415 @HasRepoPermissionAnyDecorator('repository.admin')
411 416 def settings_get(self):
412 417 return self._settings_get()
413 418
414 419 @LoginRequired()
415 420 @HasRepoPermissionAnyDecorator('repository.admin')
416 421 @CSRFRequired()
417 422 def settings_post(self):
418 423 return self._settings_post()
419 424
420 425 @LoginRequired()
421 426 @HasRepoPermissionAnyDecorator('repository.admin')
422 427 def new_integration(self):
423 428 return self._new_integration()
424 429
425 430
426 431 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
427 432 def load_default_context(self):
428 433 c = self._get_local_tmpl_context()
429 434 c.repo = self.repo
430 435 c.repo_group = self.repo_group
431 436 c.navlist = navigation_list(self.request)
432 437 self._register_global_c(c)
433 438 return c
434 439
435 440 @LoginRequired()
436 441 @HasRepoGroupPermissionAnyDecorator('group.admin')
437 442 def integration_list(self):
438 443 return self._integration_list()
439 444
440 445 @LoginRequired()
441 446 @HasRepoGroupPermissionAnyDecorator('group.admin')
442 447 def settings_get(self):
443 448 return self._settings_get()
444 449
445 450 @LoginRequired()
446 451 @HasRepoGroupPermissionAnyDecorator('group.admin')
447 452 @CSRFRequired()
448 453 def settings_post(self):
449 454 return self._settings_post()
450 455
451 456 @LoginRequired()
452 457 @HasRepoGroupPermissionAnyDecorator('group.admin')
453 458 def new_integration(self):
454 459 return self._new_integration()
@@ -1,2400 +1,2403 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'readme-box';
15 15 @import 'progress-bar';
16 16
17 17 @import 'type';
18 18 @import 'alerts';
19 19 @import 'buttons';
20 20 @import 'tags';
21 21 @import 'code-block';
22 22 @import 'examples';
23 23 @import 'login';
24 24 @import 'main-content';
25 25 @import 'select2';
26 26 @import 'comments';
27 27 @import 'panels-bootstrap';
28 28 @import 'panels';
29 29 @import 'deform';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .clipboard-action {
104 104 cursor: pointer;
105 105 }
106 106
107 107 ul.simple-list{
108 108 list-style: none;
109 109 margin: 0;
110 110 padding: 0;
111 111 }
112 112
113 113 .main-content {
114 114 padding-bottom: @pagepadding;
115 115 }
116 116
117 117 .wide-mode-wrapper {
118 118 max-width:4000px !important;
119 119 }
120 120
121 121 .wrapper {
122 122 position: relative;
123 123 max-width: @wrapper-maxwidth;
124 124 margin: 0 auto;
125 125 }
126 126
127 127 #content {
128 128 clear: both;
129 129 padding: 0 @contentpadding;
130 130 }
131 131
132 132 .advanced-settings-fields{
133 133 input{
134 134 margin-left: @textmargin;
135 135 margin-right: @padding/2;
136 136 }
137 137 }
138 138
139 139 .cs_files_title {
140 140 margin: @pagepadding 0 0;
141 141 }
142 142
143 143 input.inline[type="file"] {
144 144 display: inline;
145 145 }
146 146
147 147 .error_page {
148 148 margin: 10% auto;
149 149
150 150 h1 {
151 151 color: @grey2;
152 152 }
153 153
154 154 .alert {
155 155 margin: @padding 0;
156 156 }
157 157
158 158 .error-branding {
159 159 font-family: @text-semibold;
160 160 color: @grey4;
161 161 }
162 162
163 163 .error_message {
164 164 font-family: @text-regular;
165 165 }
166 166
167 167 .sidebar {
168 168 min-height: 275px;
169 169 margin: 0;
170 170 padding: 0 0 @sidebarpadding @sidebarpadding;
171 171 border: none;
172 172 }
173 173
174 174 .main-content {
175 175 position: relative;
176 176 margin: 0 @sidebarpadding @sidebarpadding;
177 177 padding: 0 0 0 @sidebarpadding;
178 178 border-left: @border-thickness solid @grey5;
179 179
180 180 @media (max-width:767px) {
181 181 clear: both;
182 182 width: 100%;
183 183 margin: 0;
184 184 border: none;
185 185 }
186 186 }
187 187
188 188 .inner-column {
189 189 float: left;
190 190 width: 29.75%;
191 191 min-height: 150px;
192 192 margin: @sidebarpadding 2% 0 0;
193 193 padding: 0 2% 0 0;
194 194 border-right: @border-thickness solid @grey5;
195 195
196 196 @media (max-width:767px) {
197 197 clear: both;
198 198 width: 100%;
199 199 border: none;
200 200 }
201 201
202 202 ul {
203 203 padding-left: 1.25em;
204 204 }
205 205
206 206 &:last-child {
207 207 margin: @sidebarpadding 0 0;
208 208 border: none;
209 209 }
210 210
211 211 h4 {
212 212 margin: 0 0 @padding;
213 213 font-family: @text-semibold;
214 214 }
215 215 }
216 216 }
217 217 .error-page-logo {
218 218 width: 130px;
219 219 height: 160px;
220 220 }
221 221
222 222 // HEADER
223 223 .header {
224 224
225 225 // TODO: johbo: Fix login pages, so that they work without a min-height
226 226 // for the header and then remove the min-height. I chose a smaller value
227 227 // intentionally here to avoid rendering issues in the main navigation.
228 228 min-height: 49px;
229 229
230 230 position: relative;
231 231 vertical-align: bottom;
232 232 padding: 0 @header-padding;
233 233 background-color: @grey2;
234 234 color: @grey5;
235 235
236 236 .title {
237 237 overflow: visible;
238 238 }
239 239
240 240 &:before,
241 241 &:after {
242 242 content: "";
243 243 clear: both;
244 244 width: 100%;
245 245 }
246 246
247 247 // TODO: johbo: Avoids breaking "Repositories" chooser
248 248 .select2-container .select2-choice .select2-arrow {
249 249 display: none;
250 250 }
251 251 }
252 252
253 253 #header-inner {
254 254 &.title {
255 255 margin: 0;
256 256 }
257 257 &:before,
258 258 &:after {
259 259 content: "";
260 260 clear: both;
261 261 }
262 262 }
263 263
264 264 // Gists
265 265 #files_data {
266 266 clear: both; //for firefox
267 267 }
268 268 #gistid {
269 269 margin-right: @padding;
270 270 }
271 271
272 272 // Global Settings Editor
273 273 .textarea.editor {
274 274 float: left;
275 275 position: relative;
276 276 max-width: @texteditor-width;
277 277
278 278 select {
279 279 position: absolute;
280 280 top:10px;
281 281 right:0;
282 282 }
283 283
284 284 .CodeMirror {
285 285 margin: 0;
286 286 }
287 287
288 288 .help-block {
289 289 margin: 0 0 @padding;
290 290 padding:.5em;
291 291 background-color: @grey6;
292 292 &.pre-formatting {
293 293 white-space: pre;
294 294 }
295 295 }
296 296 }
297 297
298 298 ul.auth_plugins {
299 299 margin: @padding 0 @padding @legend-width;
300 300 padding: 0;
301 301
302 302 li {
303 303 margin-bottom: @padding;
304 304 line-height: 1em;
305 305 list-style-type: none;
306 306
307 307 .auth_buttons .btn {
308 308 margin-right: @padding;
309 309 }
310 310
311 311 &:before { content: none; }
312 312 }
313 313 }
314 314
315 315
316 316 // My Account PR list
317 317
318 318 #show_closed {
319 319 margin: 0 1em 0 0;
320 320 }
321 321
322 322 .pullrequestlist {
323 323 .closed {
324 324 background-color: @grey6;
325 325 }
326 326 .td-status {
327 327 padding-left: .5em;
328 328 }
329 329 .log-container .truncate {
330 330 height: 2.75em;
331 331 white-space: pre-line;
332 332 }
333 333 table.rctable .user {
334 334 padding-left: 0;
335 335 }
336 336 table.rctable {
337 337 td.td-description,
338 338 .rc-user {
339 339 min-width: auto;
340 340 }
341 341 }
342 342 }
343 343
344 344 // Pull Requests
345 345
346 346 .pullrequests_section_head {
347 347 display: block;
348 348 clear: both;
349 349 margin: @padding 0;
350 350 font-family: @text-bold;
351 351 }
352 352
353 353 .pr-origininfo, .pr-targetinfo {
354 354 position: relative;
355 355
356 356 .tag {
357 357 display: inline-block;
358 358 margin: 0 1em .5em 0;
359 359 }
360 360
361 361 .clone-url {
362 362 display: inline-block;
363 363 margin: 0 0 .5em 0;
364 364 padding: 0;
365 365 line-height: 1.2em;
366 366 }
367 367 }
368 368
369 369 .pr-mergeinfo {
370 370 min-width: 95% !important;
371 371 padding: 0 !important;
372 372 border: 0;
373 373 }
374 374 .pr-mergeinfo-copy {
375 375 padding: 0 0;
376 376 }
377 377
378 378 .pr-pullinfo {
379 379 min-width: 95% !important;
380 380 padding: 0 !important;
381 381 border: 0;
382 382 }
383 383 .pr-pullinfo-copy {
384 384 padding: 0 0;
385 385 }
386 386
387 387
388 388 #pr-title-input {
389 389 width: 72%;
390 390 font-size: 1em;
391 391 font-family: @text-bold;
392 392 margin: 0;
393 393 padding: 0 0 0 @padding/4;
394 394 line-height: 1.7em;
395 395 color: @text-color;
396 396 letter-spacing: .02em;
397 397 }
398 398
399 399 #pullrequest_title {
400 400 width: 100%;
401 401 box-sizing: border-box;
402 402 }
403 403
404 404 #pr_open_message {
405 405 border: @border-thickness solid #fff;
406 406 border-radius: @border-radius;
407 407 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
408 408 text-align: left;
409 409 overflow: hidden;
410 410 }
411 411
412 412 .pr-submit-button {
413 413 float: right;
414 414 margin: 0 0 0 5px;
415 415 }
416 416
417 417 .pr-spacing-container {
418 418 padding: 20px;
419 419 clear: both
420 420 }
421 421
422 422 #pr-description-input {
423 423 margin-bottom: 0;
424 424 }
425 425
426 426 .pr-description-label {
427 427 vertical-align: top;
428 428 }
429 429
430 430 .perms_section_head {
431 431 min-width: 625px;
432 432
433 433 h2 {
434 434 margin-bottom: 0;
435 435 }
436 436
437 437 .label-checkbox {
438 438 float: left;
439 439 }
440 440
441 441 &.field {
442 442 margin: @space 0 @padding;
443 443 }
444 444
445 445 &:first-child.field {
446 446 margin-top: 0;
447 447
448 448 .label {
449 449 margin-top: 0;
450 450 padding-top: 0;
451 451 }
452 452
453 453 .radios {
454 454 padding-top: 0;
455 455 }
456 456 }
457 457
458 458 .radios {
459 459 position: relative;
460 460 width: 405px;
461 461 }
462 462 }
463 463
464 464 //--- MODULES ------------------//
465 465
466 466
467 467 // Server Announcement
468 468 #server-announcement {
469 469 width: 95%;
470 470 margin: @padding auto;
471 471 padding: @padding;
472 472 border-width: 2px;
473 473 border-style: solid;
474 474 .border-radius(2px);
475 475 font-family: @text-bold;
476 476
477 477 &.info { border-color: @alert4; background-color: @alert4-inner; }
478 478 &.warning { border-color: @alert3; background-color: @alert3-inner; }
479 479 &.error { border-color: @alert2; background-color: @alert2-inner; }
480 480 &.success { border-color: @alert1; background-color: @alert1-inner; }
481 481 &.neutral { border-color: @grey3; background-color: @grey6; }
482 482 }
483 483
484 484 // Fixed Sidebar Column
485 485 .sidebar-col-wrapper {
486 486 padding-left: @sidebar-all-width;
487 487
488 488 .sidebar {
489 489 width: @sidebar-width;
490 490 margin-left: -@sidebar-all-width;
491 491 }
492 492 }
493 493
494 494 .sidebar-col-wrapper.scw-small {
495 495 padding-left: @sidebar-small-all-width;
496 496
497 497 .sidebar {
498 498 width: @sidebar-small-width;
499 499 margin-left: -@sidebar-small-all-width;
500 500 }
501 501 }
502 502
503 503
504 504 // FOOTER
505 505 #footer {
506 506 padding: 0;
507 507 text-align: center;
508 508 vertical-align: middle;
509 509 color: @grey2;
510 510 background-color: @grey6;
511 511
512 512 p {
513 513 margin: 0;
514 514 padding: 1em;
515 515 line-height: 1em;
516 516 }
517 517
518 518 .server-instance { //server instance
519 519 display: none;
520 520 }
521 521
522 522 .title {
523 523 float: none;
524 524 margin: 0 auto;
525 525 }
526 526 }
527 527
528 528 button.close {
529 529 padding: 0;
530 530 cursor: pointer;
531 531 background: transparent;
532 532 border: 0;
533 533 .box-shadow(none);
534 534 -webkit-appearance: none;
535 535 }
536 536
537 537 .close {
538 538 float: right;
539 539 font-size: 21px;
540 540 font-family: @text-bootstrap;
541 541 line-height: 1em;
542 542 font-weight: bold;
543 543 color: @grey2;
544 544
545 545 &:hover,
546 546 &:focus {
547 547 color: @grey1;
548 548 text-decoration: none;
549 549 cursor: pointer;
550 550 }
551 551 }
552 552
553 553 // GRID
554 554 .sorting,
555 555 .sorting_desc,
556 556 .sorting_asc {
557 557 cursor: pointer;
558 558 }
559 559 .sorting_desc:after {
560 560 content: "\00A0\25B2";
561 561 font-size: .75em;
562 562 }
563 563 .sorting_asc:after {
564 564 content: "\00A0\25BC";
565 565 font-size: .68em;
566 566 }
567 567
568 568
569 569 .user_auth_tokens {
570 570
571 571 &.truncate {
572 572 white-space: nowrap;
573 573 overflow: hidden;
574 574 text-overflow: ellipsis;
575 575 }
576 576
577 577 .fields .field .input {
578 578 margin: 0;
579 579 }
580 580
581 581 input#description {
582 582 width: 100px;
583 583 margin: 0;
584 584 }
585 585
586 586 .drop-menu {
587 587 // TODO: johbo: Remove this, should work out of the box when
588 588 // having multiple inputs inline
589 589 margin: 0 0 0 5px;
590 590 }
591 591 }
592 592 #user_list_table {
593 593 .closed {
594 594 background-color: @grey6;
595 595 }
596 596 }
597 597
598 598
599 599 input {
600 600 &.disabled {
601 601 opacity: .5;
602 602 }
603 603 }
604 604
605 605 // remove extra padding in firefox
606 606 input::-moz-focus-inner { border:0; padding:0 }
607 607
608 608 .adjacent input {
609 609 margin-bottom: @padding;
610 610 }
611 611
612 612 .permissions_boxes {
613 613 display: block;
614 614 }
615 615
616 616 //TODO: lisa: this should be in tables
617 617 .show_more_col {
618 618 width: 20px;
619 619 }
620 620
621 621 //FORMS
622 622
623 623 .medium-inline,
624 624 input#description.medium-inline {
625 625 display: inline;
626 626 width: @medium-inline-input-width;
627 627 min-width: 100px;
628 628 }
629 629
630 630 select {
631 631 //reset
632 632 -webkit-appearance: none;
633 633 -moz-appearance: none;
634 634
635 635 display: inline-block;
636 636 height: 28px;
637 637 width: auto;
638 638 margin: 0 @padding @padding 0;
639 639 padding: 0 18px 0 8px;
640 640 line-height:1em;
641 641 font-size: @basefontsize;
642 642 border: @border-thickness solid @rcblue;
643 643 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
644 644 color: @rcblue;
645 645
646 646 &:after {
647 647 content: "\00A0\25BE";
648 648 }
649 649
650 650 &:focus {
651 651 outline: none;
652 652 }
653 653 }
654 654
655 655 option {
656 656 &:focus {
657 657 outline: none;
658 658 }
659 659 }
660 660
661 661 input,
662 662 textarea {
663 663 padding: @input-padding;
664 664 border: @input-border-thickness solid @border-highlight-color;
665 665 .border-radius (@border-radius);
666 666 font-family: @text-light;
667 667 font-size: @basefontsize;
668 668
669 669 &.input-sm {
670 670 padding: 5px;
671 671 }
672 672
673 673 &#description {
674 674 min-width: @input-description-minwidth;
675 675 min-height: 1em;
676 676 padding: 10px;
677 677 }
678 678 }
679 679
680 680 .field-sm {
681 681 input,
682 682 textarea {
683 683 padding: 5px;
684 684 }
685 685 }
686 686
687 687 textarea {
688 688 display: block;
689 689 clear: both;
690 690 width: 100%;
691 691 min-height: 100px;
692 692 margin-bottom: @padding;
693 693 .box-sizing(border-box);
694 694 overflow: auto;
695 695 }
696 696
697 697 label {
698 698 font-family: @text-light;
699 699 }
700 700
701 701 // GRAVATARS
702 702 // centers gravatar on username to the right
703 703
704 704 .gravatar {
705 705 display: inline;
706 706 min-width: 16px;
707 707 min-height: 16px;
708 708 margin: -5px 0;
709 709 padding: 0;
710 710 line-height: 1em;
711 711 border: 1px solid @grey4;
712 712 box-sizing: content-box;
713 713
714 714 &.gravatar-large {
715 715 margin: -0.5em .25em -0.5em 0;
716 716 }
717 717
718 718 & + .user {
719 719 display: inline;
720 720 margin: 0;
721 721 padding: 0 0 0 .17em;
722 722 line-height: 1em;
723 723 }
724 724 }
725 725
726 726 .user-inline-data {
727 727 display: inline-block;
728 728 float: left;
729 729 padding-left: .5em;
730 730 line-height: 1.3em;
731 731 }
732 732
733 733 .rc-user { // gravatar + user wrapper
734 734 float: left;
735 735 position: relative;
736 736 min-width: 100px;
737 737 max-width: 200px;
738 738 min-height: (@gravatar-size + @border-thickness * 2); // account for border
739 739 display: block;
740 740 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
741 741
742 742
743 743 .gravatar {
744 744 display: block;
745 745 position: absolute;
746 746 top: 0;
747 747 left: 0;
748 748 min-width: @gravatar-size;
749 749 min-height: @gravatar-size;
750 750 margin: 0;
751 751 }
752 752
753 753 .user {
754 754 display: block;
755 755 max-width: 175px;
756 756 padding-top: 2px;
757 757 overflow: hidden;
758 758 text-overflow: ellipsis;
759 759 }
760 760 }
761 761
762 762 .gist-gravatar,
763 763 .journal_container {
764 764 .gravatar-large {
765 765 margin: 0 .5em -10px 0;
766 766 }
767 767 }
768 768
769 769
770 770 // ADMIN SETTINGS
771 771
772 772 // Tag Patterns
773 773 .tag_patterns {
774 774 .tag_input {
775 775 margin-bottom: @padding;
776 776 }
777 777 }
778 778
779 779 .locked_input {
780 780 position: relative;
781 781
782 782 input {
783 783 display: inline;
784 784 margin: 3px 5px 0px 0px;
785 785 }
786 786
787 787 br {
788 788 display: none;
789 789 }
790 790
791 791 .error-message {
792 792 float: left;
793 793 width: 100%;
794 794 }
795 795
796 796 .lock_input_button {
797 797 display: inline;
798 798 }
799 799
800 800 .help-block {
801 801 clear: both;
802 802 }
803 803 }
804 804
805 805 // Notifications
806 806
807 807 .notifications_buttons {
808 808 margin: 0 0 @space 0;
809 809 padding: 0;
810 810
811 811 .btn {
812 812 display: inline-block;
813 813 }
814 814 }
815 815
816 816 .notification-list {
817 817
818 818 div {
819 819 display: inline-block;
820 820 vertical-align: middle;
821 821 }
822 822
823 823 .container {
824 824 display: block;
825 825 margin: 0 0 @padding 0;
826 826 }
827 827
828 828 .delete-notifications {
829 829 margin-left: @padding;
830 830 text-align: right;
831 831 cursor: pointer;
832 832 }
833 833
834 834 .read-notifications {
835 835 margin-left: @padding/2;
836 836 text-align: right;
837 837 width: 35px;
838 838 cursor: pointer;
839 839 }
840 840
841 841 .icon-minus-sign {
842 842 color: @alert2;
843 843 }
844 844
845 845 .icon-ok-sign {
846 846 color: @alert1;
847 847 }
848 848 }
849 849
850 850 .user_settings {
851 851 float: left;
852 852 clear: both;
853 853 display: block;
854 854 width: 100%;
855 855
856 856 .gravatar_box {
857 857 margin-bottom: @padding;
858 858
859 859 &:after {
860 860 content: " ";
861 861 clear: both;
862 862 width: 100%;
863 863 }
864 864 }
865 865
866 866 .fields .field {
867 867 clear: both;
868 868 }
869 869 }
870 870
871 871 .advanced_settings {
872 872 margin-bottom: @space;
873 873
874 874 .help-block {
875 875 margin-left: 0;
876 876 }
877 877
878 878 button + .help-block {
879 879 margin-top: @padding;
880 880 }
881 881 }
882 882
883 883 // admin settings radio buttons and labels
884 884 .label-2 {
885 885 float: left;
886 886 width: @label2-width;
887 887
888 888 label {
889 889 color: @grey1;
890 890 }
891 891 }
892 892 .checkboxes {
893 893 float: left;
894 894 width: @checkboxes-width;
895 895 margin-bottom: @padding;
896 896
897 897 .checkbox {
898 898 width: 100%;
899 899
900 900 label {
901 901 margin: 0;
902 902 padding: 0;
903 903 }
904 904 }
905 905
906 906 .checkbox + .checkbox {
907 907 display: inline-block;
908 908 }
909 909
910 910 label {
911 911 margin-right: 1em;
912 912 }
913 913 }
914 914
915 915 // CHANGELOG
916 916 .container_header {
917 917 float: left;
918 918 display: block;
919 919 width: 100%;
920 920 margin: @padding 0 @padding;
921 921
922 922 #filter_changelog {
923 923 float: left;
924 924 margin-right: @padding;
925 925 }
926 926
927 927 .breadcrumbs_light {
928 928 display: inline-block;
929 929 }
930 930 }
931 931
932 932 .info_box {
933 933 float: right;
934 934 }
935 935
936 936
937 937 #graph_nodes {
938 938 padding-top: 43px;
939 939 }
940 940
941 941 #graph_content{
942 942
943 943 // adjust for table headers so that graph renders properly
944 944 // #graph_nodes padding - table cell padding
945 945 padding-top: (@space - (@basefontsize * 2.4));
946 946
947 947 &.graph_full_width {
948 948 width: 100%;
949 949 max-width: 100%;
950 950 }
951 951 }
952 952
953 953 #graph {
954 954 .flag_status {
955 955 margin: 0;
956 956 }
957 957
958 958 .pagination-left {
959 959 float: left;
960 960 clear: both;
961 961 }
962 962
963 963 .log-container {
964 964 max-width: 345px;
965 965
966 966 .message{
967 967 max-width: 340px;
968 968 }
969 969 }
970 970
971 971 .graph-col-wrapper {
972 972 padding-left: 110px;
973 973
974 974 #graph_nodes {
975 975 width: 100px;
976 976 margin-left: -110px;
977 977 float: left;
978 978 clear: left;
979 979 }
980 980 }
981 981
982 982 .load-more-commits {
983 983 text-align: center;
984 984 }
985 985 .load-more-commits:hover {
986 986 background-color: @grey7;
987 987 }
988 988 .load-more-commits {
989 989 a {
990 990 display: block;
991 991 }
992 992 }
993 993 }
994 994
995 995 #filter_changelog {
996 996 float: left;
997 997 }
998 998
999 999
1000 1000 //--- THEME ------------------//
1001 1001
1002 1002 #logo {
1003 1003 float: left;
1004 1004 margin: 9px 0 0 0;
1005 1005
1006 1006 .header {
1007 1007 background-color: transparent;
1008 1008 }
1009 1009
1010 1010 a {
1011 1011 display: inline-block;
1012 1012 }
1013 1013
1014 1014 img {
1015 1015 height:30px;
1016 1016 }
1017 1017 }
1018 1018
1019 1019 .logo-wrapper {
1020 1020 float:left;
1021 1021 }
1022 1022
1023 1023 .branding{
1024 1024 float: left;
1025 1025 padding: 9px 2px;
1026 1026 line-height: 1em;
1027 1027 font-size: @navigation-fontsize;
1028 1028 }
1029 1029
1030 1030 img {
1031 1031 border: none;
1032 1032 outline: none;
1033 1033 }
1034 1034 user-profile-header
1035 1035 label {
1036 1036
1037 1037 input[type="checkbox"] {
1038 1038 margin-right: 1em;
1039 1039 }
1040 1040 input[type="radio"] {
1041 1041 margin-right: 1em;
1042 1042 }
1043 1043 }
1044 1044
1045 1045 .flag_status {
1046 1046 margin: 2px 8px 6px 2px;
1047 1047 &.under_review {
1048 1048 .circle(5px, @alert3);
1049 1049 }
1050 1050 &.approved {
1051 1051 .circle(5px, @alert1);
1052 1052 }
1053 1053 &.rejected,
1054 1054 &.forced_closed{
1055 1055 .circle(5px, @alert2);
1056 1056 }
1057 1057 &.not_reviewed {
1058 1058 .circle(5px, @grey5);
1059 1059 }
1060 1060 }
1061 1061
1062 1062 .flag_status_comment_box {
1063 1063 margin: 5px 6px 0px 2px;
1064 1064 }
1065 1065 .test_pattern_preview {
1066 1066 margin: @space 0;
1067 1067
1068 1068 p {
1069 1069 margin-bottom: 0;
1070 1070 border-bottom: @border-thickness solid @border-default-color;
1071 1071 color: @grey3;
1072 1072 }
1073 1073
1074 1074 .btn {
1075 1075 margin-bottom: @padding;
1076 1076 }
1077 1077 }
1078 1078 #test_pattern_result {
1079 1079 display: none;
1080 1080 &:extend(pre);
1081 1081 padding: .9em;
1082 1082 color: @grey3;
1083 1083 background-color: @grey7;
1084 1084 border-right: @border-thickness solid @border-default-color;
1085 1085 border-bottom: @border-thickness solid @border-default-color;
1086 1086 border-left: @border-thickness solid @border-default-color;
1087 1087 }
1088 1088
1089 1089 #repo_vcs_settings {
1090 1090 #inherit_overlay_vcs_default {
1091 1091 display: none;
1092 1092 }
1093 1093 #inherit_overlay_vcs_custom {
1094 1094 display: custom;
1095 1095 }
1096 1096 &.inherited {
1097 1097 #inherit_overlay_vcs_default {
1098 1098 display: block;
1099 1099 }
1100 1100 #inherit_overlay_vcs_custom {
1101 1101 display: none;
1102 1102 }
1103 1103 }
1104 1104 }
1105 1105
1106 1106 .issue-tracker-link {
1107 1107 color: @rcblue;
1108 1108 }
1109 1109
1110 1110 // Issue Tracker Table Show/Hide
1111 1111 #repo_issue_tracker {
1112 1112 #inherit_overlay {
1113 1113 display: none;
1114 1114 }
1115 1115 #custom_overlay {
1116 1116 display: custom;
1117 1117 }
1118 1118 &.inherited {
1119 1119 #inherit_overlay {
1120 1120 display: block;
1121 1121 }
1122 1122 #custom_overlay {
1123 1123 display: none;
1124 1124 }
1125 1125 }
1126 1126 }
1127 1127 table.issuetracker {
1128 1128 &.readonly {
1129 1129 tr, td {
1130 1130 color: @grey3;
1131 1131 }
1132 1132 }
1133 1133 .edit {
1134 1134 display: none;
1135 1135 }
1136 1136 .editopen {
1137 1137 .edit {
1138 1138 display: inline;
1139 1139 }
1140 1140 .entry {
1141 1141 display: none;
1142 1142 }
1143 1143 }
1144 1144 tr td.td-action {
1145 1145 min-width: 117px;
1146 1146 }
1147 1147 td input {
1148 1148 max-width: none;
1149 1149 min-width: 30px;
1150 1150 width: 80%;
1151 1151 }
1152 1152 .issuetracker_pref input {
1153 1153 width: 40%;
1154 1154 }
1155 1155 input.edit_issuetracker_update {
1156 1156 margin-right: 0;
1157 1157 width: auto;
1158 1158 }
1159 1159 }
1160 1160
1161 1161 table.integrations {
1162 1162 .td-icon {
1163 1163 width: 20px;
1164 1164 .integration-icon {
1165 1165 height: 20px;
1166 1166 width: 20px;
1167 1167 }
1168 1168 }
1169 1169 }
1170 1170
1171 1171 .integrations {
1172 1172 a.integration-box {
1173 1173 color: @text-color;
1174 1174 &:hover {
1175 1175 .panel {
1176 1176 background: #fbfbfb;
1177 1177 }
1178 1178 }
1179 1179 .integration-icon {
1180 1180 width: 30px;
1181 1181 height: 30px;
1182 1182 margin-right: 20px;
1183 1183 float: left;
1184 1184 }
1185 1185
1186 1186 .panel-body {
1187 1187 padding: 10px;
1188 1188 }
1189 1189 .panel {
1190 1190 margin-bottom: 10px;
1191 1191 }
1192 1192 h2 {
1193 1193 display: inline-block;
1194 1194 margin: 0;
1195 1195 min-width: 140px;
1196 1196 }
1197 1197 }
1198 a.integration-box.dummy-integration {
1199 color: @grey4
1200 }
1198 1201 }
1199 1202
1200 1203 //Permissions Settings
1201 1204 #add_perm {
1202 1205 margin: 0 0 @padding;
1203 1206 cursor: pointer;
1204 1207 }
1205 1208
1206 1209 .perm_ac {
1207 1210 input {
1208 1211 width: 95%;
1209 1212 }
1210 1213 }
1211 1214
1212 1215 .autocomplete-suggestions {
1213 1216 width: auto !important; // overrides autocomplete.js
1214 1217 margin: 0;
1215 1218 border: @border-thickness solid @rcblue;
1216 1219 border-radius: @border-radius;
1217 1220 color: @rcblue;
1218 1221 background-color: white;
1219 1222 }
1220 1223 .autocomplete-selected {
1221 1224 background: #F0F0F0;
1222 1225 }
1223 1226 .ac-container-wrap {
1224 1227 margin: 0;
1225 1228 padding: 8px;
1226 1229 border-bottom: @border-thickness solid @rclightblue;
1227 1230 list-style-type: none;
1228 1231 cursor: pointer;
1229 1232
1230 1233 &:hover {
1231 1234 background-color: @rclightblue;
1232 1235 }
1233 1236
1234 1237 img {
1235 1238 height: @gravatar-size;
1236 1239 width: @gravatar-size;
1237 1240 margin-right: 1em;
1238 1241 }
1239 1242
1240 1243 strong {
1241 1244 font-weight: normal;
1242 1245 }
1243 1246 }
1244 1247
1245 1248 // Settings Dropdown
1246 1249 .user-menu .container {
1247 1250 padding: 0 4px;
1248 1251 margin: 0;
1249 1252 }
1250 1253
1251 1254 .user-menu .gravatar {
1252 1255 cursor: pointer;
1253 1256 }
1254 1257
1255 1258 .codeblock {
1256 1259 margin-bottom: @padding;
1257 1260 clear: both;
1258 1261
1259 1262 .stats{
1260 1263 overflow: hidden;
1261 1264 }
1262 1265
1263 1266 .message{
1264 1267 textarea{
1265 1268 margin: 0;
1266 1269 }
1267 1270 }
1268 1271
1269 1272 .code-header {
1270 1273 .stats {
1271 1274 line-height: 2em;
1272 1275
1273 1276 .revision_id {
1274 1277 margin-left: 0;
1275 1278 }
1276 1279 .buttons {
1277 1280 padding-right: 0;
1278 1281 }
1279 1282 }
1280 1283
1281 1284 .item{
1282 1285 margin-right: 0.5em;
1283 1286 }
1284 1287 }
1285 1288
1286 1289 #editor_container{
1287 1290 position: relative;
1288 1291 margin: @padding;
1289 1292 }
1290 1293 }
1291 1294
1292 1295 #file_history_container {
1293 1296 display: none;
1294 1297 }
1295 1298
1296 1299 .file-history-inner {
1297 1300 margin-bottom: 10px;
1298 1301 }
1299 1302
1300 1303 // Pull Requests
1301 1304 .summary-details {
1302 1305 width: 72%;
1303 1306 }
1304 1307 .pr-summary {
1305 1308 border-bottom: @border-thickness solid @grey5;
1306 1309 margin-bottom: @space;
1307 1310 }
1308 1311 .reviewers-title {
1309 1312 width: 25%;
1310 1313 min-width: 200px;
1311 1314 }
1312 1315 .reviewers {
1313 1316 width: 25%;
1314 1317 min-width: 200px;
1315 1318 }
1316 1319 .reviewers ul li {
1317 1320 position: relative;
1318 1321 width: 100%;
1319 1322 margin-bottom: 8px;
1320 1323 }
1321 1324
1322 1325 .reviewer_entry {
1323 1326 min-height: 55px;
1324 1327 }
1325 1328
1326 1329 .reviewers_member {
1327 1330 width: 100%;
1328 1331 overflow: auto;
1329 1332 }
1330 1333 .reviewer_reason {
1331 1334 padding-left: 20px;
1332 1335 }
1333 1336 .reviewer_status {
1334 1337 display: inline-block;
1335 1338 vertical-align: top;
1336 1339 width: 7%;
1337 1340 min-width: 20px;
1338 1341 height: 1.2em;
1339 1342 margin-top: 3px;
1340 1343 line-height: 1em;
1341 1344 }
1342 1345
1343 1346 .reviewer_name {
1344 1347 display: inline-block;
1345 1348 max-width: 83%;
1346 1349 padding-right: 20px;
1347 1350 vertical-align: middle;
1348 1351 line-height: 1;
1349 1352
1350 1353 .rc-user {
1351 1354 min-width: 0;
1352 1355 margin: -2px 1em 0 0;
1353 1356 }
1354 1357
1355 1358 .reviewer {
1356 1359 float: left;
1357 1360 }
1358 1361 }
1359 1362
1360 1363 .reviewer_member_mandatory,
1361 1364 .reviewer_member_mandatory_remove,
1362 1365 .reviewer_member_remove {
1363 1366 position: absolute;
1364 1367 right: 0;
1365 1368 top: 0;
1366 1369 width: 16px;
1367 1370 margin-bottom: 10px;
1368 1371 padding: 0;
1369 1372 color: black;
1370 1373 }
1371 1374
1372 1375 .reviewer_member_mandatory_remove {
1373 1376 color: @grey4;
1374 1377 }
1375 1378
1376 1379 .reviewer_member_mandatory {
1377 1380 padding-top:20px;
1378 1381 }
1379 1382
1380 1383 .reviewer_member_status {
1381 1384 margin-top: 5px;
1382 1385 }
1383 1386 .pr-summary #summary{
1384 1387 width: 100%;
1385 1388 }
1386 1389 .pr-summary .action_button:hover {
1387 1390 border: 0;
1388 1391 cursor: pointer;
1389 1392 }
1390 1393 .pr-details-title {
1391 1394 padding-bottom: 8px;
1392 1395 border-bottom: @border-thickness solid @grey5;
1393 1396
1394 1397 .action_button.disabled {
1395 1398 color: @grey4;
1396 1399 cursor: inherit;
1397 1400 }
1398 1401 .action_button {
1399 1402 color: @rcblue;
1400 1403 }
1401 1404 }
1402 1405 .pr-details-content {
1403 1406 margin-top: @textmargin;
1404 1407 margin-bottom: @textmargin;
1405 1408 }
1406 1409 .pr-description {
1407 1410 white-space:pre-wrap;
1408 1411 }
1409 1412
1410 1413 .pr-reviewer-rules {
1411 1414 padding: 10px 0px 20px 0px;
1412 1415 }
1413 1416
1414 1417 .group_members {
1415 1418 margin-top: 0;
1416 1419 padding: 0;
1417 1420 list-style: outside none none;
1418 1421
1419 1422 img {
1420 1423 height: @gravatar-size;
1421 1424 width: @gravatar-size;
1422 1425 margin-right: .5em;
1423 1426 margin-left: 3px;
1424 1427 }
1425 1428
1426 1429 .to-delete {
1427 1430 .user {
1428 1431 text-decoration: line-through;
1429 1432 }
1430 1433 }
1431 1434 }
1432 1435
1433 1436 .compare_view_commits_title {
1434 1437 .disabled {
1435 1438 cursor: inherit;
1436 1439 &:hover{
1437 1440 background-color: inherit;
1438 1441 color: inherit;
1439 1442 }
1440 1443 }
1441 1444 }
1442 1445
1443 1446 .subtitle-compare {
1444 1447 margin: -15px 0px 0px 0px;
1445 1448 }
1446 1449
1447 1450 .comments-summary-td {
1448 1451 border-top: 1px dashed @grey5;
1449 1452 }
1450 1453
1451 1454 // new entry in group_members
1452 1455 .td-author-new-entry {
1453 1456 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1454 1457 }
1455 1458
1456 1459 .usergroup_member_remove {
1457 1460 width: 16px;
1458 1461 margin-bottom: 10px;
1459 1462 padding: 0;
1460 1463 color: black !important;
1461 1464 cursor: pointer;
1462 1465 }
1463 1466
1464 1467 .reviewer_ac .ac-input {
1465 1468 width: 92%;
1466 1469 margin-bottom: 1em;
1467 1470 }
1468 1471
1469 1472 .compare_view_commits tr{
1470 1473 height: 20px;
1471 1474 }
1472 1475 .compare_view_commits td {
1473 1476 vertical-align: top;
1474 1477 padding-top: 10px;
1475 1478 }
1476 1479 .compare_view_commits .author {
1477 1480 margin-left: 5px;
1478 1481 }
1479 1482
1480 1483 .compare_view_commits {
1481 1484 .color-a {
1482 1485 color: @alert1;
1483 1486 }
1484 1487
1485 1488 .color-c {
1486 1489 color: @color3;
1487 1490 }
1488 1491
1489 1492 .color-r {
1490 1493 color: @color5;
1491 1494 }
1492 1495
1493 1496 .color-a-bg {
1494 1497 background-color: @alert1;
1495 1498 }
1496 1499
1497 1500 .color-c-bg {
1498 1501 background-color: @alert3;
1499 1502 }
1500 1503
1501 1504 .color-r-bg {
1502 1505 background-color: @alert2;
1503 1506 }
1504 1507
1505 1508 .color-a-border {
1506 1509 border: 1px solid @alert1;
1507 1510 }
1508 1511
1509 1512 .color-c-border {
1510 1513 border: 1px solid @alert3;
1511 1514 }
1512 1515
1513 1516 .color-r-border {
1514 1517 border: 1px solid @alert2;
1515 1518 }
1516 1519
1517 1520 .commit-change-indicator {
1518 1521 width: 15px;
1519 1522 height: 15px;
1520 1523 position: relative;
1521 1524 left: 15px;
1522 1525 }
1523 1526
1524 1527 .commit-change-content {
1525 1528 text-align: center;
1526 1529 vertical-align: middle;
1527 1530 line-height: 15px;
1528 1531 }
1529 1532 }
1530 1533
1531 1534 .compare_view_files {
1532 1535 width: 100%;
1533 1536
1534 1537 td {
1535 1538 vertical-align: middle;
1536 1539 }
1537 1540 }
1538 1541
1539 1542 .compare_view_filepath {
1540 1543 color: @grey1;
1541 1544 }
1542 1545
1543 1546 .show_more {
1544 1547 display: inline-block;
1545 1548 position: relative;
1546 1549 vertical-align: middle;
1547 1550 width: 4px;
1548 1551 height: @basefontsize;
1549 1552
1550 1553 &:after {
1551 1554 content: "\00A0\25BE";
1552 1555 display: inline-block;
1553 1556 width:10px;
1554 1557 line-height: 5px;
1555 1558 font-size: 12px;
1556 1559 cursor: pointer;
1557 1560 }
1558 1561 }
1559 1562
1560 1563 .journal_more .show_more {
1561 1564 display: inline;
1562 1565
1563 1566 &:after {
1564 1567 content: none;
1565 1568 }
1566 1569 }
1567 1570
1568 1571 .open .show_more:after,
1569 1572 .select2-dropdown-open .show_more:after {
1570 1573 .rotate(180deg);
1571 1574 margin-left: 4px;
1572 1575 }
1573 1576
1574 1577
1575 1578 .compare_view_commits .collapse_commit:after {
1576 1579 cursor: pointer;
1577 1580 content: "\00A0\25B4";
1578 1581 margin-left: -3px;
1579 1582 font-size: 17px;
1580 1583 color: @grey4;
1581 1584 }
1582 1585
1583 1586 .diff_links {
1584 1587 margin-left: 8px;
1585 1588 }
1586 1589
1587 1590 div.ancestor {
1588 1591 margin: -30px 0px;
1589 1592 }
1590 1593
1591 1594 .cs_icon_td input[type="checkbox"] {
1592 1595 display: none;
1593 1596 }
1594 1597
1595 1598 .cs_icon_td .expand_file_icon:after {
1596 1599 cursor: pointer;
1597 1600 content: "\00A0\25B6";
1598 1601 font-size: 12px;
1599 1602 color: @grey4;
1600 1603 }
1601 1604
1602 1605 .cs_icon_td .collapse_file_icon:after {
1603 1606 cursor: pointer;
1604 1607 content: "\00A0\25BC";
1605 1608 font-size: 12px;
1606 1609 color: @grey4;
1607 1610 }
1608 1611
1609 1612 /*new binary
1610 1613 NEW_FILENODE = 1
1611 1614 DEL_FILENODE = 2
1612 1615 MOD_FILENODE = 3
1613 1616 RENAMED_FILENODE = 4
1614 1617 COPIED_FILENODE = 5
1615 1618 CHMOD_FILENODE = 6
1616 1619 BIN_FILENODE = 7
1617 1620 */
1618 1621 .cs_files_expand {
1619 1622 font-size: @basefontsize + 5px;
1620 1623 line-height: 1.8em;
1621 1624 float: right;
1622 1625 }
1623 1626
1624 1627 .cs_files_expand span{
1625 1628 color: @rcblue;
1626 1629 cursor: pointer;
1627 1630 }
1628 1631 .cs_files {
1629 1632 clear: both;
1630 1633 padding-bottom: @padding;
1631 1634
1632 1635 .cur_cs {
1633 1636 margin: 10px 2px;
1634 1637 font-weight: bold;
1635 1638 }
1636 1639
1637 1640 .node {
1638 1641 float: left;
1639 1642 }
1640 1643
1641 1644 .changes {
1642 1645 float: right;
1643 1646 color: white;
1644 1647 font-size: @basefontsize - 4px;
1645 1648 margin-top: 4px;
1646 1649 opacity: 0.6;
1647 1650 filter: Alpha(opacity=60); /* IE8 and earlier */
1648 1651
1649 1652 .added {
1650 1653 background-color: @alert1;
1651 1654 float: left;
1652 1655 text-align: center;
1653 1656 }
1654 1657
1655 1658 .deleted {
1656 1659 background-color: @alert2;
1657 1660 float: left;
1658 1661 text-align: center;
1659 1662 }
1660 1663
1661 1664 .bin {
1662 1665 background-color: @alert1;
1663 1666 text-align: center;
1664 1667 }
1665 1668
1666 1669 /*new binary*/
1667 1670 .bin.bin1 {
1668 1671 background-color: @alert1;
1669 1672 text-align: center;
1670 1673 }
1671 1674
1672 1675 /*deleted binary*/
1673 1676 .bin.bin2 {
1674 1677 background-color: @alert2;
1675 1678 text-align: center;
1676 1679 }
1677 1680
1678 1681 /*mod binary*/
1679 1682 .bin.bin3 {
1680 1683 background-color: @grey2;
1681 1684 text-align: center;
1682 1685 }
1683 1686
1684 1687 /*rename file*/
1685 1688 .bin.bin4 {
1686 1689 background-color: @alert4;
1687 1690 text-align: center;
1688 1691 }
1689 1692
1690 1693 /*copied file*/
1691 1694 .bin.bin5 {
1692 1695 background-color: @alert4;
1693 1696 text-align: center;
1694 1697 }
1695 1698
1696 1699 /*chmod file*/
1697 1700 .bin.bin6 {
1698 1701 background-color: @grey2;
1699 1702 text-align: center;
1700 1703 }
1701 1704 }
1702 1705 }
1703 1706
1704 1707 .cs_files .cs_added, .cs_files .cs_A,
1705 1708 .cs_files .cs_added, .cs_files .cs_M,
1706 1709 .cs_files .cs_added, .cs_files .cs_D {
1707 1710 height: 16px;
1708 1711 padding-right: 10px;
1709 1712 margin-top: 7px;
1710 1713 text-align: left;
1711 1714 }
1712 1715
1713 1716 .cs_icon_td {
1714 1717 min-width: 16px;
1715 1718 width: 16px;
1716 1719 }
1717 1720
1718 1721 .pull-request-merge {
1719 1722 border: 1px solid @grey5;
1720 1723 padding: 10px 0px 20px;
1721 1724 margin-top: 10px;
1722 1725 margin-bottom: 20px;
1723 1726 }
1724 1727
1725 1728 .pull-request-merge ul {
1726 1729 padding: 0px 0px;
1727 1730 }
1728 1731
1729 1732 .pull-request-merge li:before{
1730 1733 content:none;
1731 1734 }
1732 1735
1733 1736 .pull-request-merge .pull-request-wrap {
1734 1737 height: auto;
1735 1738 padding: 0px 0px;
1736 1739 text-align: right;
1737 1740 }
1738 1741
1739 1742 .pull-request-merge span {
1740 1743 margin-right: 5px;
1741 1744 }
1742 1745
1743 1746 .pull-request-merge-actions {
1744 1747 min-height: 30px;
1745 1748 padding: 0px 0px;
1746 1749 }
1747 1750
1748 1751 .pull-request-merge-info {
1749 1752 padding: 0px 5px 5px 0px;
1750 1753 }
1751 1754
1752 1755 .merge-status {
1753 1756 margin-right: 5px;
1754 1757 }
1755 1758
1756 1759 .merge-message {
1757 1760 font-size: 1.2em
1758 1761 }
1759 1762
1760 1763 .merge-message.success i,
1761 1764 .merge-icon.success i {
1762 1765 color:@alert1;
1763 1766 }
1764 1767
1765 1768 .merge-message.warning i,
1766 1769 .merge-icon.warning i {
1767 1770 color: @alert3;
1768 1771 }
1769 1772
1770 1773 .merge-message.error i,
1771 1774 .merge-icon.error i {
1772 1775 color:@alert2;
1773 1776 }
1774 1777
1775 1778 .pr-versions {
1776 1779 font-size: 1.1em;
1777 1780
1778 1781 table {
1779 1782 padding: 0px 5px;
1780 1783 }
1781 1784
1782 1785 td {
1783 1786 line-height: 15px;
1784 1787 }
1785 1788
1786 1789 .flag_status {
1787 1790 margin: 0;
1788 1791 }
1789 1792
1790 1793 .compare-radio-button {
1791 1794 position: relative;
1792 1795 top: -3px;
1793 1796 }
1794 1797 }
1795 1798
1796 1799
1797 1800 #close_pull_request {
1798 1801 margin-right: 0px;
1799 1802 }
1800 1803
1801 1804 .empty_data {
1802 1805 color: @grey4;
1803 1806 }
1804 1807
1805 1808 #changeset_compare_view_content {
1806 1809 margin-bottom: @space;
1807 1810 clear: both;
1808 1811 width: 100%;
1809 1812 box-sizing: border-box;
1810 1813 .border-radius(@border-radius);
1811 1814
1812 1815 .help-block {
1813 1816 margin: @padding 0;
1814 1817 color: @text-color;
1815 1818 &.pre-formatting {
1816 1819 white-space: pre;
1817 1820 }
1818 1821 }
1819 1822
1820 1823 .empty_data {
1821 1824 margin: @padding 0;
1822 1825 }
1823 1826
1824 1827 .alert {
1825 1828 margin-bottom: @space;
1826 1829 }
1827 1830 }
1828 1831
1829 1832 .table_disp {
1830 1833 .status {
1831 1834 width: auto;
1832 1835
1833 1836 .flag_status {
1834 1837 float: left;
1835 1838 }
1836 1839 }
1837 1840 }
1838 1841
1839 1842
1840 1843 .creation_in_progress {
1841 1844 color: @grey4
1842 1845 }
1843 1846
1844 1847 .status_box_menu {
1845 1848 margin: 0;
1846 1849 }
1847 1850
1848 1851 .notification-table{
1849 1852 margin-bottom: @space;
1850 1853 display: table;
1851 1854 width: 100%;
1852 1855
1853 1856 .container{
1854 1857 display: table-row;
1855 1858
1856 1859 .notification-header{
1857 1860 border-bottom: @border-thickness solid @border-default-color;
1858 1861 }
1859 1862
1860 1863 .notification-subject{
1861 1864 display: table-cell;
1862 1865 }
1863 1866 }
1864 1867 }
1865 1868
1866 1869 // Notifications
1867 1870 .notification-header{
1868 1871 display: table;
1869 1872 width: 100%;
1870 1873 padding: floor(@basefontsize/2) 0;
1871 1874 line-height: 1em;
1872 1875
1873 1876 .desc, .delete-notifications, .read-notifications{
1874 1877 display: table-cell;
1875 1878 text-align: left;
1876 1879 }
1877 1880
1878 1881 .desc{
1879 1882 width: 1163px;
1880 1883 }
1881 1884
1882 1885 .delete-notifications, .read-notifications{
1883 1886 width: 35px;
1884 1887 min-width: 35px; //fixes when only one button is displayed
1885 1888 }
1886 1889 }
1887 1890
1888 1891 .notification-body {
1889 1892 .markdown-block,
1890 1893 .rst-block {
1891 1894 padding: @padding 0;
1892 1895 }
1893 1896
1894 1897 .notification-subject {
1895 1898 padding: @textmargin 0;
1896 1899 border-bottom: @border-thickness solid @border-default-color;
1897 1900 }
1898 1901 }
1899 1902
1900 1903
1901 1904 .notifications_buttons{
1902 1905 float: right;
1903 1906 }
1904 1907
1905 1908 #notification-status{
1906 1909 display: inline;
1907 1910 }
1908 1911
1909 1912 // Repositories
1910 1913
1911 1914 #summary.fields{
1912 1915 display: table;
1913 1916
1914 1917 .field{
1915 1918 display: table-row;
1916 1919
1917 1920 .label-summary{
1918 1921 display: table-cell;
1919 1922 min-width: @label-summary-minwidth;
1920 1923 padding-top: @padding/2;
1921 1924 padding-bottom: @padding/2;
1922 1925 padding-right: @padding/2;
1923 1926 }
1924 1927
1925 1928 .input{
1926 1929 display: table-cell;
1927 1930 padding: @padding/2;
1928 1931
1929 1932 input{
1930 1933 min-width: 29em;
1931 1934 padding: @padding/4;
1932 1935 }
1933 1936 }
1934 1937 .statistics, .downloads{
1935 1938 .disabled{
1936 1939 color: @grey4;
1937 1940 }
1938 1941 }
1939 1942 }
1940 1943 }
1941 1944
1942 1945 #summary{
1943 1946 width: 70%;
1944 1947 }
1945 1948
1946 1949
1947 1950 // Journal
1948 1951 .journal.title {
1949 1952 h5 {
1950 1953 float: left;
1951 1954 margin: 0;
1952 1955 width: 70%;
1953 1956 }
1954 1957
1955 1958 ul {
1956 1959 float: right;
1957 1960 display: inline-block;
1958 1961 margin: 0;
1959 1962 width: 30%;
1960 1963 text-align: right;
1961 1964
1962 1965 li {
1963 1966 display: inline;
1964 1967 font-size: @journal-fontsize;
1965 1968 line-height: 1em;
1966 1969
1967 1970 &:before { content: none; }
1968 1971 }
1969 1972 }
1970 1973 }
1971 1974
1972 1975 .filterexample {
1973 1976 position: absolute;
1974 1977 top: 95px;
1975 1978 left: @contentpadding;
1976 1979 color: @rcblue;
1977 1980 font-size: 11px;
1978 1981 font-family: @text-regular;
1979 1982 cursor: help;
1980 1983
1981 1984 &:hover {
1982 1985 color: @rcdarkblue;
1983 1986 }
1984 1987
1985 1988 @media (max-width:768px) {
1986 1989 position: relative;
1987 1990 top: auto;
1988 1991 left: auto;
1989 1992 display: block;
1990 1993 }
1991 1994 }
1992 1995
1993 1996
1994 1997 #journal{
1995 1998 margin-bottom: @space;
1996 1999
1997 2000 .journal_day{
1998 2001 margin-bottom: @textmargin/2;
1999 2002 padding-bottom: @textmargin/2;
2000 2003 font-size: @journal-fontsize;
2001 2004 border-bottom: @border-thickness solid @border-default-color;
2002 2005 }
2003 2006
2004 2007 .journal_container{
2005 2008 margin-bottom: @space;
2006 2009
2007 2010 .journal_user{
2008 2011 display: inline-block;
2009 2012 }
2010 2013 .journal_action_container{
2011 2014 display: block;
2012 2015 margin-top: @textmargin;
2013 2016
2014 2017 div{
2015 2018 display: inline;
2016 2019 }
2017 2020
2018 2021 div.journal_action_params{
2019 2022 display: block;
2020 2023 }
2021 2024
2022 2025 div.journal_repo:after{
2023 2026 content: "\A";
2024 2027 white-space: pre;
2025 2028 }
2026 2029
2027 2030 div.date{
2028 2031 display: block;
2029 2032 margin-bottom: @textmargin;
2030 2033 }
2031 2034 }
2032 2035 }
2033 2036 }
2034 2037
2035 2038 // Files
2036 2039 .edit-file-title {
2037 2040 border-bottom: @border-thickness solid @border-default-color;
2038 2041
2039 2042 .breadcrumbs {
2040 2043 margin-bottom: 0;
2041 2044 }
2042 2045 }
2043 2046
2044 2047 .edit-file-fieldset {
2045 2048 margin-top: @sidebarpadding;
2046 2049
2047 2050 .fieldset {
2048 2051 .left-label {
2049 2052 width: 13%;
2050 2053 }
2051 2054 .right-content {
2052 2055 width: 87%;
2053 2056 max-width: 100%;
2054 2057 }
2055 2058 .filename-label {
2056 2059 margin-top: 13px;
2057 2060 }
2058 2061 .commit-message-label {
2059 2062 margin-top: 4px;
2060 2063 }
2061 2064 .file-upload-input {
2062 2065 input {
2063 2066 display: none;
2064 2067 }
2065 2068 margin-top: 10px;
2066 2069 }
2067 2070 .file-upload-label {
2068 2071 margin-top: 10px;
2069 2072 }
2070 2073 p {
2071 2074 margin-top: 5px;
2072 2075 }
2073 2076
2074 2077 }
2075 2078 .custom-path-link {
2076 2079 margin-left: 5px;
2077 2080 }
2078 2081 #commit {
2079 2082 resize: vertical;
2080 2083 }
2081 2084 }
2082 2085
2083 2086 .delete-file-preview {
2084 2087 max-height: 250px;
2085 2088 }
2086 2089
2087 2090 .new-file,
2088 2091 #filter_activate,
2089 2092 #filter_deactivate {
2090 2093 float: left;
2091 2094 margin: 0 0 0 15px;
2092 2095 }
2093 2096
2094 2097 h3.files_location{
2095 2098 line-height: 2.4em;
2096 2099 }
2097 2100
2098 2101 .browser-nav {
2099 2102 display: table;
2100 2103 margin-bottom: @space;
2101 2104
2102 2105
2103 2106 .info_box {
2104 2107 display: inline-table;
2105 2108 height: 2.5em;
2106 2109
2107 2110 .browser-cur-rev, .info_box_elem {
2108 2111 display: table-cell;
2109 2112 vertical-align: middle;
2110 2113 }
2111 2114
2112 2115 .info_box_elem {
2113 2116 border-top: @border-thickness solid @rcblue;
2114 2117 border-bottom: @border-thickness solid @rcblue;
2115 2118
2116 2119 #at_rev, a {
2117 2120 padding: 0.6em 0.9em;
2118 2121 margin: 0;
2119 2122 .box-shadow(none);
2120 2123 border: 0;
2121 2124 height: 12px;
2122 2125 }
2123 2126
2124 2127 input#at_rev {
2125 2128 max-width: 50px;
2126 2129 text-align: right;
2127 2130 }
2128 2131
2129 2132 &.previous {
2130 2133 border: @border-thickness solid @rcblue;
2131 2134 .disabled {
2132 2135 color: @grey4;
2133 2136 cursor: not-allowed;
2134 2137 }
2135 2138 }
2136 2139
2137 2140 &.next {
2138 2141 border: @border-thickness solid @rcblue;
2139 2142 .disabled {
2140 2143 color: @grey4;
2141 2144 cursor: not-allowed;
2142 2145 }
2143 2146 }
2144 2147 }
2145 2148
2146 2149 .browser-cur-rev {
2147 2150
2148 2151 span{
2149 2152 margin: 0;
2150 2153 color: @rcblue;
2151 2154 height: 12px;
2152 2155 display: inline-block;
2153 2156 padding: 0.7em 1em ;
2154 2157 border: @border-thickness solid @rcblue;
2155 2158 margin-right: @padding;
2156 2159 }
2157 2160 }
2158 2161 }
2159 2162
2160 2163 .search_activate {
2161 2164 display: table-cell;
2162 2165 vertical-align: middle;
2163 2166
2164 2167 input, label{
2165 2168 margin: 0;
2166 2169 padding: 0;
2167 2170 }
2168 2171
2169 2172 input{
2170 2173 margin-left: @textmargin;
2171 2174 }
2172 2175
2173 2176 }
2174 2177 }
2175 2178
2176 2179 .browser-cur-rev{
2177 2180 margin-bottom: @textmargin;
2178 2181 }
2179 2182
2180 2183 #node_filter_box_loading{
2181 2184 .info_text;
2182 2185 }
2183 2186
2184 2187 .browser-search {
2185 2188 margin: -25px 0px 5px 0px;
2186 2189 }
2187 2190
2188 2191 .node-filter {
2189 2192 font-size: @repo-title-fontsize;
2190 2193 padding: 4px 0px 0px 0px;
2191 2194
2192 2195 .node-filter-path {
2193 2196 float: left;
2194 2197 color: @grey4;
2195 2198 }
2196 2199 .node-filter-input {
2197 2200 float: left;
2198 2201 margin: -2px 0px 0px 2px;
2199 2202 input {
2200 2203 padding: 2px;
2201 2204 border: none;
2202 2205 font-size: @repo-title-fontsize;
2203 2206 }
2204 2207 }
2205 2208 }
2206 2209
2207 2210
2208 2211 .browser-result{
2209 2212 td a{
2210 2213 margin-left: 0.5em;
2211 2214 display: inline-block;
2212 2215
2213 2216 em{
2214 2217 font-family: @text-bold;
2215 2218 }
2216 2219 }
2217 2220 }
2218 2221
2219 2222 .browser-highlight{
2220 2223 background-color: @grey5-alpha;
2221 2224 }
2222 2225
2223 2226
2224 2227 // Search
2225 2228
2226 2229 .search-form{
2227 2230 #q {
2228 2231 width: @search-form-width;
2229 2232 }
2230 2233 .fields{
2231 2234 margin: 0 0 @space;
2232 2235 }
2233 2236
2234 2237 label{
2235 2238 display: inline-block;
2236 2239 margin-right: @textmargin;
2237 2240 padding-top: 0.25em;
2238 2241 }
2239 2242
2240 2243
2241 2244 .results{
2242 2245 clear: both;
2243 2246 margin: 0 0 @padding;
2244 2247 }
2245 2248 }
2246 2249
2247 2250 div.search-feedback-items {
2248 2251 display: inline-block;
2249 2252 padding:0px 0px 0px 96px;
2250 2253 }
2251 2254
2252 2255 div.search-code-body {
2253 2256 background-color: #ffffff; padding: 5px 0 5px 10px;
2254 2257 pre {
2255 2258 .match { background-color: #faffa6;}
2256 2259 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2257 2260 }
2258 2261 }
2259 2262
2260 2263 .expand_commit.search {
2261 2264 .show_more.open {
2262 2265 height: auto;
2263 2266 max-height: none;
2264 2267 }
2265 2268 }
2266 2269
2267 2270 .search-results {
2268 2271
2269 2272 h2 {
2270 2273 margin-bottom: 0;
2271 2274 }
2272 2275 .codeblock {
2273 2276 border: none;
2274 2277 background: transparent;
2275 2278 }
2276 2279
2277 2280 .codeblock-header {
2278 2281 border: none;
2279 2282 background: transparent;
2280 2283 }
2281 2284
2282 2285 .code-body {
2283 2286 border: @border-thickness solid @border-default-color;
2284 2287 .border-radius(@border-radius);
2285 2288 }
2286 2289
2287 2290 .td-commit {
2288 2291 &:extend(pre);
2289 2292 border-bottom: @border-thickness solid @border-default-color;
2290 2293 }
2291 2294
2292 2295 .message {
2293 2296 height: auto;
2294 2297 max-width: 350px;
2295 2298 white-space: normal;
2296 2299 text-overflow: initial;
2297 2300 overflow: visible;
2298 2301
2299 2302 .match { background-color: #faffa6;}
2300 2303 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2301 2304 }
2302 2305
2303 2306 }
2304 2307
2305 2308 table.rctable td.td-search-results div {
2306 2309 max-width: 100%;
2307 2310 }
2308 2311
2309 2312 #tip-box, .tip-box{
2310 2313 padding: @menupadding/2;
2311 2314 display: block;
2312 2315 border: @border-thickness solid @border-highlight-color;
2313 2316 .border-radius(@border-radius);
2314 2317 background-color: white;
2315 2318 z-index: 99;
2316 2319 white-space: pre-wrap;
2317 2320 }
2318 2321
2319 2322 #linktt {
2320 2323 width: 79px;
2321 2324 }
2322 2325
2323 2326 #help_kb .modal-content{
2324 2327 max-width: 750px;
2325 2328 margin: 10% auto;
2326 2329
2327 2330 table{
2328 2331 td,th{
2329 2332 border-bottom: none;
2330 2333 line-height: 2.5em;
2331 2334 }
2332 2335 th{
2333 2336 padding-bottom: @textmargin/2;
2334 2337 }
2335 2338 td.keys{
2336 2339 text-align: center;
2337 2340 }
2338 2341 }
2339 2342
2340 2343 .block-left{
2341 2344 width: 45%;
2342 2345 margin-right: 5%;
2343 2346 }
2344 2347 .modal-footer{
2345 2348 clear: both;
2346 2349 }
2347 2350 .key.tag{
2348 2351 padding: 0.5em;
2349 2352 background-color: @rcblue;
2350 2353 color: white;
2351 2354 border-color: @rcblue;
2352 2355 .box-shadow(none);
2353 2356 }
2354 2357 }
2355 2358
2356 2359
2357 2360
2358 2361 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2359 2362
2360 2363 @import 'statistics-graph';
2361 2364 @import 'tables';
2362 2365 @import 'forms';
2363 2366 @import 'diff';
2364 2367 @import 'summary';
2365 2368 @import 'navigation';
2366 2369
2367 2370 //--- SHOW/HIDE SECTIONS --//
2368 2371
2369 2372 .btn-collapse {
2370 2373 float: right;
2371 2374 text-align: right;
2372 2375 font-family: @text-light;
2373 2376 font-size: @basefontsize;
2374 2377 cursor: pointer;
2375 2378 border: none;
2376 2379 color: @rcblue;
2377 2380 }
2378 2381
2379 2382 table.rctable,
2380 2383 table.dataTable {
2381 2384 .btn-collapse {
2382 2385 float: right;
2383 2386 text-align: right;
2384 2387 }
2385 2388 }
2386 2389
2387 2390
2388 2391 // TODO: johbo: Fix for IE10, this avoids that we see a border
2389 2392 // and padding around checkboxes and radio boxes. Move to the right place,
2390 2393 // or better: Remove this once we did the form refactoring.
2391 2394 input[type=checkbox],
2392 2395 input[type=radio] {
2393 2396 padding: 0;
2394 2397 border: none;
2395 2398 }
2396 2399
2397 2400 .toggle-ajax-spinner{
2398 2401 height: 16px;
2399 2402 width: 16px;
2400 2403 }
@@ -1,254 +1,256 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 %if c.repo:
6 6 ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))}
7 7 %elif c.repo_group:
8 8 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
9 9 &raquo;
10 10 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
13 13 %else:
14 14 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
15 15 &raquo;
16 16 ${h.link_to(_('Settings'),h.url('admin_settings'))}
17 17 %endif
18 18 %if c.current_IntegrationType:
19 19 &raquo;
20 20 %if c.repo:
21 21 ${h.link_to(_('Integrations'),
22 22 request.route_path(route_name='repo_integrations_home',
23 23 repo_name=c.repo.repo_name))}
24 24 %elif c.repo_group:
25 25 ${h.link_to(_('Integrations'),
26 26 request.route_path(route_name='repo_group_integrations_home',
27 27 repo_group_name=c.repo_group.group_name))}
28 28 %else:
29 29 ${h.link_to(_('Integrations'),
30 30 request.route_path(route_name='global_integrations_home'))}
31 31 %endif
32 32 &raquo;
33 33 ${c.current_IntegrationType.display_name}
34 34 %else:
35 35 &raquo;
36 36 ${_('Integrations')}
37 37 %endif
38 38 </%def>
39 39
40 40 <div class="panel panel-default">
41 41 <div class="panel-heading">
42 42 <h3 class="panel-title">
43 43 %if c.repo:
44 44 ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
45 45 %elif c.repo_group:
46 46 ${_('Current Integrations for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
47 47 %else:
48 48 ${_('Current Integrations')}
49 49 %endif
50 50 </h3>
51 51 </div>
52 52 <div class="panel-body">
53 53 <%
54 54 if c.repo:
55 55 home_url = request.route_path('repo_integrations_home',
56 56 repo_name=c.repo.repo_name)
57 57 elif c.repo_group:
58 58 home_url = request.route_path('repo_group_integrations_home',
59 59 repo_group_name=c.repo_group.group_name)
60 60 else:
61 61 home_url = request.route_path('global_integrations_home')
62 62 %>
63 63
64 64 <a href="${home_url}" class="btn ${not c.current_IntegrationType and 'btn-primary' or ''}">${_('All')}</a>
65 65
66 66 %for integration_key, IntegrationType in c.available_integrations.items():
67 <%
68 if c.repo:
69 list_url = request.route_path('repo_integrations_list',
70 repo_name=c.repo.repo_name,
71 integration=integration_key)
72 elif c.repo_group:
73 list_url = request.route_path('repo_group_integrations_list',
74 repo_group_name=c.repo_group.group_name,
75 integration=integration_key)
76 else:
77 list_url = request.route_path('global_integrations_list',
78 integration=integration_key)
79 %>
80 <a href="${list_url}"
81 class="btn ${c.current_IntegrationType and integration_key == c.current_IntegrationType.key and 'btn-primary' or ''}">
82 ${IntegrationType.display_name}
83 </a>
67 % if not IntegrationType.is_dummy:
68 <%
69 if c.repo:
70 list_url = request.route_path('repo_integrations_list',
71 repo_name=c.repo.repo_name,
72 integration=integration_key)
73 elif c.repo_group:
74 list_url = request.route_path('repo_group_integrations_list',
75 repo_group_name=c.repo_group.group_name,
76 integration=integration_key)
77 else:
78 list_url = request.route_path('global_integrations_list',
79 integration=integration_key)
80 %>
81 <a href="${list_url}"
82 class="btn ${c.current_IntegrationType and integration_key == c.current_IntegrationType.key and 'btn-primary' or ''}">
83 ${IntegrationType.display_name}
84 </a>
85 % endif
84 86 %endfor
85 87
86 88 <%
87 89 integration_type = c.current_IntegrationType and c.current_IntegrationType.display_name or ''
88 90
89 91 if c.repo:
90 92 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
91 93 elif c.repo_group:
92 94 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
93 95 else:
94 96 create_url = h.route_path('global_integrations_new')
95 97 %>
96 98 <p class="pull-right">
97 99 <a href="${create_url}" class="btn btn-small btn-success">${_(u'Create new integration')}</a>
98 100 </p>
99 101
100 102 <table class="rctable integrations">
101 103 <thead>
102 104 <tr>
103 105 <th><a href="?sort=enabled:${c.rev_sort_dir}">${_('Enabled')}</a></th>
104 106 <th><a href="?sort=name:${c.rev_sort_dir}">${_('Name')}</a></th>
105 107 <th colspan="2"><a href="?sort=integration_type:${c.rev_sort_dir}">${_('Type')}</a></th>
106 108 <th><a href="?sort=scope:${c.rev_sort_dir}">${_('Scope')}</a></th>
107 109 <th>${_('Actions')}</th>
108 110 <th></th>
109 111 </tr>
110 112 </thead>
111 113 <tbody>
112 114 %if not c.integrations_list:
113 115 <tr>
114 116 <td colspan="7">
115 117
116 118 %if c.repo:
117 119 ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)}
118 120 %elif c.repo_group:
119 121 ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
120 122 %else:
121 123 ${_('No {type} integrations exist yet.').format(type=integration_type)}
122 124 %endif
123 125
124 126 %if c.current_IntegrationType:
125 127 <%
126 128 if c.repo:
127 129 create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=c.current_IntegrationType.key)
128 130 elif c.repo_group:
129 131 create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=c.current_IntegrationType.key)
130 132 else:
131 133 create_url = h.route_path('global_integrations_create', integration=c.current_IntegrationType.key)
132 134 %>
133 135 %endif
134 136
135 137 <a href="${create_url}">${_(u'Create one')}</a>
136 138 </td>
137 139 </tr>
138 140 %endif
139 141 %for IntegrationType, integration in c.integrations_list:
140 142 <tr id="integration_${integration.integration_id}">
141 143 <td class="td-enabled">
142 144 %if integration.enabled:
143 145 <div class="flag_status approved pull-left"></div>
144 146 %else:
145 147 <div class="flag_status rejected pull-left"></div>
146 148 %endif
147 149 </td>
148 150 <td class="td-description">
149 151 ${integration.name}
150 152 </td>
151 153 <td class="td-icon">
152 154 %if integration.integration_type in c.available_integrations:
153 155 <div class="integration-icon">
154 156 ${c.available_integrations[integration.integration_type].icon|n}
155 157 </div>
156 158 %else:
157 159 ?
158 160 %endif
159 161 </td>
160 162 <td class="td-type">
161 163 ${integration.integration_type}
162 164 </td>
163 165 <td class="td-scope">
164 166 %if integration.repo:
165 167 <a href="${h.route_path('repo_summary', repo_name=integration.repo.repo_name)}">
166 168 ${_('repo')}:${integration.repo.repo_name}
167 169 </a>
168 170 %elif integration.repo_group:
169 171 <a href="${h.route_path('repo_group_home', repo_group_name=integration.repo_group.group_name)}">
170 172 ${_('repogroup')}:${integration.repo_group.group_name}
171 173 %if integration.child_repos_only:
172 174 ${_('child repos only')}
173 175 %else:
174 176 ${_('cascade to all')}
175 177 %endif
176 178 </a>
177 179 %else:
178 180 %if integration.child_repos_only:
179 181 ${_('top level repos only')}
180 182 %else:
181 183 ${_('global')}
182 184 %endif
183 185 </td>
184 186 %endif
185 187 <td class="td-action">
186 188 %if not IntegrationType:
187 189 ${_('unknown integration')}
188 190 %else:
189 191 <%
190 192 if c.repo:
191 193 edit_url = request.route_path('repo_integrations_edit',
192 194 repo_name=c.repo.repo_name,
193 195 integration=integration.integration_type,
194 196 integration_id=integration.integration_id)
195 197 elif c.repo_group:
196 198 edit_url = request.route_path('repo_group_integrations_edit',
197 199 repo_group_name=c.repo_group.group_name,
198 200 integration=integration.integration_type,
199 201 integration_id=integration.integration_id)
200 202 else:
201 203 edit_url = request.route_path('global_integrations_edit',
202 204 integration=integration.integration_type,
203 205 integration_id=integration.integration_id)
204 206 %>
205 207 <div class="grid_edit">
206 208 <a href="${edit_url}">${_('Edit')}</a>
207 209 </div>
208 210 <div class="grid_delete">
209 211 <a href="${edit_url}"
210 212 class="btn btn-link btn-danger delete_integration_entry"
211 213 data-desc="${integration.name}"
212 214 data-uid="${integration.integration_id}">
213 215 ${_('Delete')}
214 216 </a>
215 217 </div>
216 218 %endif
217 219 </td>
218 220 </tr>
219 221 %endfor
220 222 <tr id="last-row"></tr>
221 223 </tbody>
222 224 </table>
223 225 <div class="integrations-paginator">
224 226 <div class="pagination-wh pagination-left">
225 227 ${c.integrations_list.pager('$link_previous ~2~ $link_next')}
226 228 </div>
227 229 </div>
228 230 </div>
229 231 </div>
230 232 <script type="text/javascript">
231 233 var delete_integration = function(entry) {
232 234 if (confirm("Confirm to remove this integration: "+$(entry).data('desc'))) {
233 235 var request = $.ajax({
234 236 type: "POST",
235 237 url: $(entry).attr('href'),
236 238 data: {
237 239 'delete': 'delete',
238 240 'csrf_token': CSRF_TOKEN
239 241 },
240 242 success: function(){
241 243 location.reload();
242 244 },
243 245 error: function(data, textStatus, errorThrown){
244 246 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
245 247 }
246 248 });
247 249 };
248 250 };
249 251
250 252 $('.delete_integration_entry').on('click', function(e){
251 253 e.preventDefault();
252 254 delete_integration(this);
253 255 });
254 256 </script> No newline at end of file
@@ -1,66 +1,68 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="widgets" file="/widgets.mako"/>
4 4
5 5 <%def name="breadcrumbs_links()">
6 6 %if c.repo:
7 7 ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))}
8 8 &raquo;
9 9 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
10 10 %elif c.repo_group:
11 11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
12 12 &raquo;
13 13 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
14 14 &raquo;
15 15 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
16 16 &raquo;
17 17 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
18 18 %else:
19 19 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
20 20 &raquo;
21 21 ${h.link_to(_('Settings'),h.url('admin_settings'))}
22 22 &raquo;
23 23 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
24 24 %endif
25 25 &raquo;
26 26 ${_('Create new integration')}
27 27 </%def>
28 28 <%widgets:panel class_='integrations'>
29 29 <%def name="title()">
30 30 %if c.repo:
31 31 ${_('Create New Integration for repository: {repo_name}').format(repo_name=c.repo.repo_name)}
32 32 %elif c.repo_group:
33 33 ${_('Create New Integration for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
34 34 %else:
35 35 ${_('Create New Global Integration')}
36 36 %endif
37 37 </%def>
38 38
39 %for integration, IntegrationType in c.available_integrations.items():
39 %for integration, IntegrationObject in c.available_integrations.items():
40 40 <%
41 41 if c.repo:
42 42 create_url = request.route_path('repo_integrations_create',
43 43 repo_name=c.repo.repo_name,
44 44 integration=integration)
45 45 elif c.repo_group:
46 46 create_url = request.route_path('repo_group_integrations_create',
47 47 repo_group_name=c.repo_group.group_name,
48 48 integration=integration)
49 49 else:
50 50 create_url = request.route_path('global_integrations_create',
51 51 integration=integration)
52 if IntegrationObject.is_dummy:
53 create_url = request.current_route_path()
52 54 %>
53 <a href="${create_url}" class="integration-box">
55 <a href="${create_url}" class="integration-box ${'dummy-integration' if IntegrationObject.is_dummy else ''}">
54 56 <%widgets:panel>
55 57 <h2>
56 58 <div class="integration-icon">
57 ${IntegrationType.icon|n}
59 ${IntegrationObject.icon|n}
58 60 </div>
59 ${IntegrationType.display_name}
61 ${IntegrationObject.display_name}
60 62 </h2>
61 ${IntegrationType.description or _('No description available')}
63 ${IntegrationObject.description or _('No description available')}
62 64 </%widgets:panel>
63 65 </a>
64 66 %endfor
65 67 <div style="clear:both"></div>
66 68 </%widgets:panel>
General Comments 0
You need to be logged in to leave comments. Login now