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),
|
||||
)
|
||||
|
||||
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):
|
||||
repository = ForeignKeyField(Repository)
|
||||
|
@ -1022,6 +1017,13 @@ class MediaType(BaseModel):
|
|||
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):
|
||||
""" LabelSourceType is an enumeration of the possible sources for a label. """
|
||||
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():
|
||||
"""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):
|
||||
"""Insert messages into the database."""
|
||||
inserted = []
|
||||
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
|
||||
|
||||
def delete_message(uuids):
|
||||
|
|
|
@ -35,6 +35,16 @@ class GlobalUserMessages(ApiResource):
|
|||
'type': 'string',
|
||||
'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',
|
||||
'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 {
|
||||
'uuid': message.uuid,
|
||||
'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='application/json')
|
||||
MediaType.create(name='text/markdown')
|
||||
|
||||
LabelSourceType.create(name='manifest')
|
||||
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),
|
||||
'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.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">
|
||||
<thead>
|
||||
<td>Message</td>
|
||||
<td>Severity</td>
|
||||
<td style="options-cols"></td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="message in messages" class="user-row">
|
||||
<td>
|
||||
{{ message.content }}
|
||||
<td class="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 class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
|
@ -54,25 +68,21 @@
|
|||
<h4 class="modal-title">Create new message</h4>
|
||||
</div>
|
||||
<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="cor-loader"></div>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="!creatingMessage && !createdMessage">
|
||||
<div class="form-group">
|
||||
<label>Message</label>
|
||||
<input class="form-control" type="text" ng-model="newMessage.content" required>
|
||||
</div>
|
||||
<label>Severity</label>
|
||||
<select class="form-control" ng-model="newMessage.severity">
|
||||
<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 class="modal-footer" ng-show="createdMessage">
|
||||
<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 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>
|
||||
|
|
|
@ -12,7 +12,10 @@ angular.module('quay').directive('globalMessageTab', function () {
|
|||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function ($scope, $element, ApiService) {
|
||||
$scope.newMessage = {};
|
||||
$scope.newMessage = {
|
||||
'media_type': 'text/markdown',
|
||||
'severity': 'info'
|
||||
};
|
||||
$scope.creatingMessage = false;
|
||||
|
||||
$scope.showCreateMessage = function () {
|
||||
|
@ -30,13 +33,17 @@ angular.module('quay').directive('globalMessageTab', function () {
|
|||
});
|
||||
|
||||
var data = {
|
||||
message: $scope.newMessage
|
||||
'message': $scope.newMessage
|
||||
};
|
||||
|
||||
ApiService.createGlobalMessage(data, null).then(function (resp) {
|
||||
$scope.creatingMessage = false;
|
||||
$scope.createdMessage = {content: $scope.newMessage.content};
|
||||
$scope.newMessage = {};
|
||||
$scope.newMessage = {
|
||||
'media_type': 'text/markdown',
|
||||
'severity': 'info'
|
||||
};
|
||||
|
||||
$('#createMessageModal').modal('hide');
|
||||
$scope.loadMessageInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -4295,6 +4295,25 @@ class TestSuperUserManagement(ApiTestCase):
|
|||
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):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
|
|
@ -4733,14 +4733,18 @@ class TestSuperUserManagement(ApiTestCase):
|
|||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# 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)
|
||||
self.assertEquals(len(json['messages']), 3)
|
||||
|
||||
self.assertEquals(len(json['messages']), 2)
|
||||
self.assertEquals(json['messages'][1]["content"], "new message")
|
||||
self.assertNotEqual(json['messages'][0]["content"], json['messages'][1]["content"])
|
||||
self.assertTrue(json['messages'][1]["uuid"])
|
||||
self.assertEquals(json['messages'][2]["content"], "new message")
|
||||
self.assertEquals(json['messages'][2]["severity"], "info")
|
||||
self.assertEquals(json['messages'][2]["media_type"], "text/plain")
|
||||
|
||||
self.assertNotEqual(json['messages'][0]["content"], json['messages'][2]["content"])
|
||||
self.assertTrue(json['messages'][2]["uuid"])
|
||||
|
||||
def test_delete_message(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
@ -4749,7 +4753,7 @@ class TestSuperUserManagement(ApiTestCase):
|
|||
|
||||
json = self.getJsonResponse(GlobalUserMessages)
|
||||
|
||||
self.assertEquals(len(json['messages']), 0)
|
||||
self.assertEquals(len(json['messages']), 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
Reference in a new issue