Merge pull request #2282 from coreos-inc/motd-updates
Severity and Markdown support in MOTD
This commit is contained in:
commit
e2748fccd9
13 changed files with 234 additions and 36 deletions
|
@ -828,11 +828,6 @@ class LogEntry(BaseModel):
|
||||||
(('repository', 'datetime', 'kind'), False),
|
(('repository', 'datetime', 'kind'), False),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Messages(BaseModel):
|
|
||||||
content = TextField()
|
|
||||||
|
|
||||||
# TODO: This should be non-nullable and indexed
|
|
||||||
uuid = CharField(default=uuid_generator, max_length=36, null=True)
|
|
||||||
|
|
||||||
class RepositoryActionCount(BaseModel):
|
class RepositoryActionCount(BaseModel):
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
|
@ -1022,6 +1017,13 @@ class MediaType(BaseModel):
|
||||||
name = CharField(index=True, unique=True)
|
name = CharField(index=True, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Messages(BaseModel):
|
||||||
|
content = TextField()
|
||||||
|
uuid = CharField(default=uuid_generator, max_length=36, index=True)
|
||||||
|
severity = CharField(default='info', index=True)
|
||||||
|
media_type = ForeignKeyField(MediaType)
|
||||||
|
|
||||||
|
|
||||||
class LabelSourceType(BaseModel):
|
class LabelSourceType(BaseModel):
|
||||||
""" LabelSourceType is an enumeration of the possible sources for a label. """
|
""" LabelSourceType is an enumeration of the possible sources for a label. """
|
||||||
name = CharField(index=True, unique=True)
|
name = CharField(index=True, unique=True)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Add severity and media_type to global messages
|
||||||
|
|
||||||
|
Revision ID: 3e8cc74a1e7b
|
||||||
|
Revises: fc47c1ec019f
|
||||||
|
Create Date: 2017-01-17 16:22:28.584237
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3e8cc74a1e7b'
|
||||||
|
down_revision = 'fc47c1ec019f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('messages', sa.Column('media_type_id', sa.Integer(), nullable=False, server_default='1'))
|
||||||
|
op.add_column('messages', sa.Column('severity', sa.String(length=255), nullable=False, server_default='info'))
|
||||||
|
op.alter_column('messages', 'uuid',
|
||||||
|
existing_type=mysql.VARCHAR(length=36),
|
||||||
|
server_default='',
|
||||||
|
nullable=False)
|
||||||
|
op.create_index('messages_media_type_id', 'messages', ['media_type_id'], unique=False)
|
||||||
|
op.create_index('messages_severity', 'messages', ['severity'], unique=False)
|
||||||
|
op.create_index('messages_uuid', 'messages', ['uuid'], unique=False)
|
||||||
|
op.create_foreign_key(op.f('fk_messages_media_type_id_mediatype'), 'messages', 'mediatype', ['media_type_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
op.bulk_insert(tables.mediatype,
|
||||||
|
[
|
||||||
|
{'name': 'text/markdown'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(op.f('fk_messages_media_type_id_mediatype'), 'messages', type_='foreignkey')
|
||||||
|
op.drop_index('messages_uuid', table_name='messages')
|
||||||
|
op.drop_index('messages_severity', table_name='messages')
|
||||||
|
op.drop_index('messages_media_type_id', table_name='messages')
|
||||||
|
op.alter_column('messages', 'uuid',
|
||||||
|
existing_type=mysql.VARCHAR(length=36),
|
||||||
|
nullable=True)
|
||||||
|
op.drop_column('messages', 'severity')
|
||||||
|
op.drop_column('messages', 'media_type_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
op.execute(tables
|
||||||
|
.mediatype
|
||||||
|
.delete()
|
||||||
|
.where(tables.
|
||||||
|
mediatype.c.name == op.inline_literal('text/markdown')))
|
|
@ -1,15 +1,20 @@
|
||||||
from data.database import Messages
|
from data.database import Messages, MediaType
|
||||||
|
|
||||||
|
|
||||||
def get_messages():
|
def get_messages():
|
||||||
"""Query the data base for messages and returns a container of database message objects"""
|
"""Query the data base for messages and returns a container of database message objects"""
|
||||||
return Messages.select()
|
return Messages.select(Messages, MediaType).join(MediaType)
|
||||||
|
|
||||||
def create(messages):
|
def create(messages):
|
||||||
"""Insert messages into the database."""
|
"""Insert messages into the database."""
|
||||||
inserted = []
|
inserted = []
|
||||||
for message in messages:
|
for message in messages:
|
||||||
inserted.append(Messages.create(content=message['content']))
|
severity = message['severity']
|
||||||
|
media_type_name = message['media_type']
|
||||||
|
media_type = MediaType.get(name=media_type_name)
|
||||||
|
|
||||||
|
inserted.append(Messages.create(content=message['content'], media_type=media_type,
|
||||||
|
severity=severity))
|
||||||
return inserted
|
return inserted
|
||||||
|
|
||||||
def delete_message(uuids):
|
def delete_message(uuids):
|
||||||
|
|
|
@ -35,6 +35,16 @@ class GlobalUserMessages(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The actual message',
|
'description': 'The actual message',
|
||||||
},
|
},
|
||||||
|
'media_type': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The media type of the message',
|
||||||
|
'enum': ['text/plain', 'text/markdown'],
|
||||||
|
},
|
||||||
|
'severity': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The severity of the message',
|
||||||
|
'enum': ['info', 'warning', 'error'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -53,6 +63,16 @@ class GlobalUserMessages(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The actual message',
|
'description': 'The actual message',
|
||||||
},
|
},
|
||||||
|
'media_type': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The media type of the message',
|
||||||
|
'enum': ['text/plain', 'text/markdown'],
|
||||||
|
},
|
||||||
|
'severity': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The severity of the message',
|
||||||
|
'enum': ['info', 'warning', 'error'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,4 +124,6 @@ def message_view(message):
|
||||||
return {
|
return {
|
||||||
'uuid': message.uuid,
|
'uuid': message.uuid,
|
||||||
'content': message.content,
|
'content': message.content,
|
||||||
|
'severity': message.severity,
|
||||||
|
'media_type': message.media_type.name,
|
||||||
}
|
}
|
||||||
|
|
|
@ -395,6 +395,7 @@ def initialize_database():
|
||||||
|
|
||||||
MediaType.create(name='text/plain')
|
MediaType.create(name='text/plain')
|
||||||
MediaType.create(name='application/json')
|
MediaType.create(name='application/json')
|
||||||
|
MediaType.create(name='text/markdown')
|
||||||
|
|
||||||
LabelSourceType.create(name='manifest')
|
LabelSourceType.create(name='manifest')
|
||||||
LabelSourceType.create(name='api', mutable=True)
|
LabelSourceType.create(name='api', mutable=True)
|
||||||
|
@ -798,7 +799,11 @@ def populate_database(minimal=False, with_storage=False):
|
||||||
'trigger_id': trigger.uuid, 'config': json.loads(trigger.config),
|
'trigger_id': trigger.uuid, 'config': json.loads(trigger.config),
|
||||||
'service': trigger.service.name})
|
'service': trigger.service.name})
|
||||||
|
|
||||||
model.message.create([{'content': 'We love you, Quay customers!'}])
|
model.message.create([{'content': 'We love you, Quay customers!', 'severity': 'info',
|
||||||
|
'media_type': 'text/plain'}])
|
||||||
|
|
||||||
|
model.message.create([{'content': 'This is a **development** install of Quay',
|
||||||
|
'severity': 'warning', 'media_type': 'text/markdown'}])
|
||||||
|
|
||||||
fake_queue = WorkQueue('fakequeue', tf)
|
fake_queue = WorkQueue('fakequeue', tf)
|
||||||
fake_queue.put(['canonical', 'job', 'name'], '{}')
|
fake_queue.put(['canonical', 'job', 'name'], '{}')
|
||||||
|
|
29
static/css/directives/ui/global-message-tab.css
Normal file
29
static/css/directives/ui/global-message-tab.css
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.global-message-tab-element .message-content p {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element .fa {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element .ci-stop {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element .fa-exclamation-triangle {
|
||||||
|
color: #E4C212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element .fa-info-circle {
|
||||||
|
color: #124fd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element label {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-message-tab-element label:first-child {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
34
static/css/directives/ui/quay-message-bar.css
Normal file
34
static/css/directives/ui/quay-message-bar.css
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
.quay-message-bar-element .markdown-view p {
|
||||||
|
margin: 0px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quay-message-bar-element .quay-service-status-description.warning {
|
||||||
|
background: #FFFBF0;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quay-message-bar-element .quay-service-status-description.warning:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f071";
|
||||||
|
font-size: 22px;
|
||||||
|
color: #E4C212;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quay-message-bar-element .quay-service-status-description.error {
|
||||||
|
background: #FFF0F0;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quay-message-bar-element .quay-service-status-description.error:before {
|
||||||
|
font-family: core-icons;
|
||||||
|
content: "\f109";
|
||||||
|
font-size: 22px;
|
||||||
|
color: red;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
|
@ -11,12 +11,26 @@
|
||||||
<table class="cor-table">
|
<table class="cor-table">
|
||||||
<thead>
|
<thead>
|
||||||
<td>Message</td>
|
<td>Message</td>
|
||||||
|
<td>Severity</td>
|
||||||
<td style="options-cols"></td>
|
<td style="options-cols"></td>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr ng-repeat="message in messages" class="user-row">
|
<tr ng-repeat="message in messages" class="user-row">
|
||||||
<td>
|
<td class="message-content">
|
||||||
{{ message.content }}
|
<span ng-switch on="message.media_type">
|
||||||
|
<span ng-switch-when="text/markdown">
|
||||||
|
<span class="markdown-view" content="message.content"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-default>{{ message.content }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="message-severity" ng-class="message.severity">
|
||||||
|
<span ng-switch on="message.severity">
|
||||||
|
<i class="fa fa-exclamation-triangle" ng-switch-when="warning"></i>
|
||||||
|
<i class="fa ci-stop" ng-switch-when="error"></i>
|
||||||
|
<i class="fa fa-info-circle" ng-switch-default></i>
|
||||||
|
</span>
|
||||||
|
{{ message.severity }}
|
||||||
</td>
|
</td>
|
||||||
<td class="options-col">
|
<td class="options-col">
|
||||||
<span class="cor-options-menu">
|
<span class="cor-options-menu">
|
||||||
|
@ -54,25 +68,21 @@
|
||||||
<h4 class="modal-title">Create new message</h4>
|
<h4 class="modal-title">Create new message</h4>
|
||||||
</div>
|
</div>
|
||||||
<form name="createMessageForm" ng-submit="createNewMessage()">
|
<form name="createMessageForm" ng-submit="createNewMessage()">
|
||||||
<div class="modal-body" ng-show="createdMessage">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<th>Message</th>
|
|
||||||
</thead>
|
|
||||||
<tr class="user-row">
|
|
||||||
<td>{{ createdMessage.content }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" ng-show="creatingMessage">
|
<div class="modal-body" ng-show="creatingMessage">
|
||||||
<div class="cor-loader"></div>
|
<div class="cor-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" ng-show="!creatingMessage && !createdMessage">
|
<div class="modal-body" ng-show="!creatingMessage && !createdMessage">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Message</label>
|
<label>Severity</label>
|
||||||
<input class="form-control" type="text" ng-model="newMessage.content" required>
|
<select class="form-control" ng-model="newMessage.severity">
|
||||||
</div>
|
<option value="info">Normal (Info)</option>
|
||||||
|
<option value="warning">Warning</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Message</label>
|
||||||
|
<div class="markdown-editor" content="newMessage.content"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" ng-show="createdMessage">
|
<div class="modal-footer" ng-show="createdMessage">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<div class="announcement inline" ng-show="messages.length">
|
<div class="announcement inline quay-message-bar-element" ng-show="messages.length">
|
||||||
<div ng-repeat="message in messages">
|
<div ng-repeat="message in messages">
|
||||||
<div class="quay-service-status-description">{{ message.content }}</div>
|
<div class="quay-service-status-description" ng-class="message.severity">
|
||||||
|
<span ng-switch on="message.media_type">
|
||||||
|
<span ng-switch-when="text/markdown">
|
||||||
|
<span class="markdown-view" content="message.content"></span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-default>{{ message.content }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,10 @@ angular.module('quay').directive('globalMessageTab', function () {
|
||||||
'isEnabled': '=isEnabled'
|
'isEnabled': '=isEnabled'
|
||||||
},
|
},
|
||||||
controller: function ($scope, $element, ApiService) {
|
controller: function ($scope, $element, ApiService) {
|
||||||
$scope.newMessage = {};
|
$scope.newMessage = {
|
||||||
|
'media_type': 'text/markdown',
|
||||||
|
'severity': 'info'
|
||||||
|
};
|
||||||
$scope.creatingMessage = false;
|
$scope.creatingMessage = false;
|
||||||
|
|
||||||
$scope.showCreateMessage = function () {
|
$scope.showCreateMessage = function () {
|
||||||
|
@ -30,13 +33,17 @@ angular.module('quay').directive('globalMessageTab', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
message: $scope.newMessage
|
'message': $scope.newMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||||
$scope.creatingMessage = false;
|
$scope.creatingMessage = false;
|
||||||
$scope.createdMessage = {content: $scope.newMessage.content};
|
$scope.newMessage = {
|
||||||
$scope.newMessage = {};
|
'media_type': 'text/markdown',
|
||||||
|
'severity': 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#createMessageModal').modal('hide');
|
||||||
$scope.loadMessageInternal();
|
$scope.loadMessageInternal();
|
||||||
}, errorHandler)
|
}, errorHandler)
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
|
@ -4295,6 +4295,25 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
self._run_test('DELETE', 204, 'devtable', None)
|
self._run_test('DELETE', 204, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserMessages(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(GlobalUserMessages)
|
||||||
|
self.message = {'message': {'content': '', 'severity': 'info', 'media_type': 'text/plain'}}
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', self.message)
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', self.message)
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 201, 'devtable', self.message)
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserMessage(ApiTestCase):
|
class TestSuperUserMessage(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -4733,14 +4733,18 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# Create a message
|
# Create a message
|
||||||
self.postResponse(GlobalUserMessages, data=dict(message={"content": "new message"}), expected_code=201)
|
message = {"content": "new message", "severity": "info", "media_type": "text/plain"}
|
||||||
|
self.postResponse(GlobalUserMessages, data=dict(message=message), expected_code=201)
|
||||||
|
|
||||||
json = self.getJsonResponse(GlobalUserMessages)
|
json = self.getJsonResponse(GlobalUserMessages)
|
||||||
|
self.assertEquals(len(json['messages']), 3)
|
||||||
|
|
||||||
self.assertEquals(len(json['messages']), 2)
|
self.assertEquals(json['messages'][2]["content"], "new message")
|
||||||
self.assertEquals(json['messages'][1]["content"], "new message")
|
self.assertEquals(json['messages'][2]["severity"], "info")
|
||||||
self.assertNotEqual(json['messages'][0]["content"], json['messages'][1]["content"])
|
self.assertEquals(json['messages'][2]["media_type"], "text/plain")
|
||||||
self.assertTrue(json['messages'][1]["uuid"])
|
|
||||||
|
self.assertNotEqual(json['messages'][0]["content"], json['messages'][2]["content"])
|
||||||
|
self.assertTrue(json['messages'][2]["uuid"])
|
||||||
|
|
||||||
def test_delete_message(self):
|
def test_delete_message(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
@ -4749,7 +4753,7 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
|
|
||||||
json = self.getJsonResponse(GlobalUserMessages)
|
json = self.getJsonResponse(GlobalUserMessages)
|
||||||
|
|
||||||
self.assertEquals(len(json['messages']), 0)
|
self.assertEquals(len(json['messages']), 1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Reference in a new issue