Initial commit

Signed-off-by: Romein van Buren <romein@vburen.nl>
This commit is contained in:
2022-06-22 17:22:19 +02:00
commit 1983f78fba
17 changed files with 3016 additions and 0 deletions

457
entities/webservice.js Normal file
View File

@ -0,0 +1,457 @@
'use strict';
const { makeId } = require('core/makeid');
const states = {
concept: 'concept',
online: 'online',
offline: 'offline',
};
module.exports = {
format: 5,
author: 'Romein van Buren',
vendor: 'Smart Yellow',
purpose: 'Store web services for use on a status page',
store: 'webservices',
forms: ({ settings }) => ({
default: {
pages: [
{ label: 'meta',
sections: [
'id',
'state',
'name',
'tags',
'channels',
],
},
{ label: 'description',
sections: [
'summary',
'visual',
'body',
],
},
{ label: 'auto testing',
sections: [
'autotestEnabled',
'lastChecked',
'autotest',
],
},
{ label: 'statistics',
sections: [
'outageStats',
'outageTable',
],
},
],
sections: {
id: {
label: 'id',
fields: [
{ key: 'id',
editor: 'string',
validator: '',
visible: false,
readonly: true,
},
],
},
state: {
label: 'status',
fields: [
{ key: 'state',
editor: 'select',
options: states,
translate: true,
},
],
},
name: {
label: 'name',
fields: [
{ key: 'name',
editor: 'string',
placeholder: 'service name',
localized: true,
},
],
},
tags: {
label: 'tags',
fields: [
{ key: 'tags',
editor: 'multiselect',
placeholder: 'enter tags...',
visible: !!(settings.serviceTags && Object.keys(settings.serviceTags).length),
options: settings.serviceTags,
},
],
},
channels: {
label: 'channels',
fields: [
{ key: 'channels',
editor: 'multiselect',
placeholder: 'enter channels...',
visible: !!(settings.channels && Object.keys(settings.channels).length),
options: settings.channels,
},
],
},
summary: {
label: 'summary',
fields: [
{ key: 'summary',
editor: 'text',
localized: true,
},
],
},
body: {
label: 'body',
fields: [
{ key: 'body',
editor: 'text',
type: 'string',
localized: true,
markup: true,
},
],
},
visual: {
label: 'visual',
fields: [
{ key: 'visual',
editor: 'file',
accept: [ 'image/*' ],
},
],
},
lastChecked: {
label: 'status last checked on',
fields: [
{ key: 'lastChecked',
editor: 'date',
readonly: true,
format: 'datetime',
},
],
},
outageStats: {
label: 'number of outages',
fields: [
{ key: 'outages',
editor: 'string',
visible: ({ newEntity }) => !newEntity,
readonly: true,
placeholder: 'all outages',
default: 0,
},
{ key: 'outagesOpen',
editor: 'string',
visible: ({ newEntity }) => !newEntity,
readonly: true,
label: 'open',
placeholder: '0',
},
{ key: 'outagesResolved',
editor: 'string',
visible: ({ newEntity }) => !newEntity,
readonly: true,
label: 'closed',
placeholder: '0',
},
],
},
outageTable: {
label: 'outage list',
fields: [
{ key: 'id',
editor: 'smartyellow/outagetable',
},
],
},
autotestEnabled: {
label: 'autotesting enabled?',
fields: [
{ key: 'autotestEnabled',
editor: 'checkbox',
},
],
},
autotest: {
label: 'endpoint requirements',
fields: [
{ key: 'autotest',
editor: 'smartyellow/autotest',
},
],
},
},
},
}),
schema: ({ settings }) => ({
id: {
type: 'string',
required: ({ newEntity }) => newEntity,
lowercase: true,
trim: true,
filter: {
title: 'id',
match: '^[a-zA-Z0-9]{6}$',
order: 999,
},
default: () => makeId(6),
validate: async ({ newValues, oldValues, newEntity, storage }) => {
if (newEntity) {
const r = storage ? await storage.store('webdesq/blog').get(newValues.id) : null;
return (r == null ? true : 'id already exists');
}
else {
// ID cannot be changed if record was already created
return (newValues.id == oldValues.id ? true : 'id cannot be changed');
}
},
},
name: {
type: 'stringset',
trim: true,
required: [ true, 'Name is missing!' ],
filter: {
title: 'title',
match: '^[a-zA-Z0-9]*',
localized: true,
},
format: {
label: 'name',
type: 'text',
sortable: 'string',
align: 'left',
minWidth: 150,
enabled: true,
priority: 1,
},
},
state: {
type: 'string',
required: true,
default: 'concept',
filter: {
title: 'state',
options: states,
},
format: {
label: 'state',
type: 'state',
align: 'left',
sortable: 'text',
sorted: 'down',
minWidth: 90,
enabled: true,
options: {
concept: {
name: 'concept',
class: 'l2',
},
online: {
name: 'online',
class: 'l4',
},
offline: {
name: 'offline',
class: 'l1',
},
},
},
},
tags: {
type: 'array',
of: 'string',
default: [],
filter: {
title: 'tags',
match: '^[a-z]',
options: async ({ storage }) => (await storage.store('smartyellow/webservice').find({ 'log.deleted': { $exists: false } }, { keys: [ 'id', 'tags' ] }).sort({ name: 1 }).toArray())
.reduce((acc, curr) => {
if (curr.tags) {
for (let i = 0; i < curr.tags.length; i++) {
acc[curr.tags[i]] = curr.tags[i];
}
}
return acc;
}, {}),
},
format: {
type: 'text',
label: 'tags',
sortable: 'text',
enabled: true,
minWidth: 100,
},
},
channels: {
type: 'array',
of: 'string',
default: () => {
const keys = settings.channels ? Object.keys(settings.channels) : [];
if (keys.length == 1) {
return [ keys[0] ];
}
else {
return [];
}
},
filter: {
title: 'channel',
match: settings.channels && Object.keys(settings.channels).length ? '^[' + Object.keys(settings.channels).join('|') + ']' : null,
},
validate: async ({ newValues }) => (newValues.channels.every(key => !!settings.channels[key]) ? true : 'One or more invalid channels'),
format: {
label: 'channels',
type: 'text',
sortable: 'text',
minWidth: 80,
enabled: true,
},
},
summary: {
type: 'stringset',
default: '',
},
body: {
type: 'stringset',
default: '',
filter: {
title: 'message contains',
match: '[a-z0-9A-Z]*',
localized: true,
},
},
visual: {
type: 'array',
of: [ 'string' ],
default: [],
skip: true,
onDataValid: async ({ newValues, storage, user }) => {
newValues.visual = newValues.visual || [];
for (let i = 0; i < newValues.visual.length; i++) {
if (newValues.visual[i].data) {
if (storage) {
// If storage is available, insert the new file into storage and collect id
const result = await storage({ user }).bucket('webdesq/media').insert({
id: makeId(6),
filename: newValues.visual[i].name,
metadata: {
contentType: newValues.visual[i].type,
},
}, newValues.visual[i].data)
.catch(error => {
if (error.code !== 'DUPLICATE_FILE') {
throw error;
}
newValues.visual[i] = error.file.id;
});
if (result) {
newValues.visual[i] = result.id;
}
}
else {
// If no storage is available, remove slot by setting it to null
newValues.visual[i] = null;
}
}
// remove empty slots in photo array
newValues.visual = newValues.visual.filter(i => i != null);
newValues.visual = [ ...new Set(newValues.visual) ];
}
},
},
outages: {
type: 'computed',
default: 0,
format: {
label: 'outages',
type: 'number',
align: 'left',
sortable: 'number',
minWidth: 100,
enabled: true,
},
generator: async ({ values, storage, user }) => {
const outages = await storage({ user })
.store('smartyellow/webserviceoutage')
.find({ services: values.id })
.toArray();
return outages.length || 0;
},
},
outagesOpen: {
type: 'computed',
default: 0,
format: {
label: 'open outages',
type: 'number',
align: 'left',
sortable: 'number',
minWidth: 100,
enabled: true,
},
generator: async ({ values, storage, user }) => {
const outages = await storage({ user })
.store('smartyellow/webserviceoutage')
.find({ services: values.id, resolved: false })
.toArray();
return outages.length || 0;
},
},
outagesResolved: {
type: 'computed',
default: 0,
format: {
label: 'resolved outages',
type: 'number',
align: 'left',
sortable: 'number',
minWidth: 100,
enabled: true,
},
generator: async ({ values, storage, user }) => {
const outages = await storage({ user })
.store('smartyellow/webserviceoutage')
.find({ services: values.id, resolved: true })
.toArray();
return outages.length || 0;
},
},
}),
};

View File

@ -0,0 +1,71 @@
'use strict';
const { makeId } = require('core/makeid');
module.exports = {
format: 5,
author: 'Romein van Buren',
vendor: 'Smart Yellow',
purpose: 'Store the heartbeat of web services',
store: 'webserviceheartbeat',
schema: () => ({
id: {
type: 'string',
required: ({ newEntity }) => newEntity,
lowercase: true,
trim: true,
filter: {
title: 'id',
match: '^[a-zA-Z0-9]{6}$',
order: 999,
},
default: () => makeId(6),
validate: async ({ newValues, oldValues, newEntity, storage }) => {
if (newEntity) {
const r = storage ? await storage.store('webdesq/blog').get(newValues.id) : null;
return (r == null ? true : 'id already exists');
}
else {
// ID cannot be changed if record was already created
return (newValues.id == oldValues.id ? true : 'id cannot be changed');
}
},
},
down: {
type: 'boolean',
default: true,
required: [ true, 'is the service up or down?' ],
format: {
type: 'state',
options: {
true: {
name: 'yes',
class: 'l1',
},
false: {
name: 'no',
class: 'l5',
},
},
},
},
webservice: {
type: 'string',
default: '',
required: [ true, 'webservice id is missing' ],
validate: async ({ storage, newValues }) => {
const r = storage ? await storage.store('smartyellow/webservice').get(newValues.webservice) : null;
return r == null ? 'service id does not exist' : true;
},
},
date: {
type: 'date',
default: '',
required: [ true, 'date is missing' ],
},
}),
};

View File

@ -0,0 +1,424 @@
'use strict';
const { makeId } = require('core/makeid');
const states = {
concept: 'concept',
online: 'online',
offline: 'offline',
};
const severity = {
major: 'major outage',
minor: 'minor outage',
scheduled: 'scheduled maintenance',
none: 'no impact on end user',
};
module.exports = {
format: 5,
author: 'Romein van Buren',
vendor: 'Smart Yellow',
purpose: 'Keep track of web service outage',
store: 'webserviceoutage',
forms: ({ settings }) => ({
default: {
pages: [
{ label: 'meta',
sections: [
'id',
'name',
'state',
'severity',
'resolved',
'services',
'tags',
],
},
{ label: 'description',
sections: [
'summary',
'visual',
'body',
],
},
{ label: 'updates',
sections: [ 'updates' ],
},
{ label: 'internal notes',
sections: [ 'notes' ],
},
],
sections: {
id: {
label: 'id',
fields: [
{ key: 'id',
editor: 'string',
validator: '',
visible: false,
readonly: true,
},
],
},
state: {
label: 'status',
fields: [
{ key: 'state',
editor: 'select',
options: states,
translate: true,
},
],
},
severity: {
label: 'severity',
fields: [
{ key: 'severity',
editor: 'select',
options: severity,
translate: true,
},
],
},
resolved: {
label: 'resolved?',
fields: [
{ key: 'resolved',
editor: 'checkbox',
},
],
},
services: {
label: 'services',
fields: [
{ key: 'services',
editor: 'multiselect',
placeholder: 'Add one or more web services this outage message belongs to.',
options: async ({ storage, user }) => await storage({ user })
.store('smartyellow/webservice')
.find({ 'log.deleted': { $exists: false } })
.toObject('id', 'name'),
},
],
},
name: {
label: 'name',
fields: [
{ key: 'name',
editor: 'string',
placeholder: 'a really brief description of the outage',
localized: true,
},
],
},
tags: {
label: 'tags',
fields: [
{ key: 'tags',
editor: 'multiselect',
placeholder: 'enter tags...',
visible: !!(settings.outageTags && Object.keys(settings.outageTags).length),
options: settings.outageTags,
},
],
},
summary: {
label: 'summary',
fields: [
{ key: 'summary',
editor: 'text',
localized: true,
},
],
},
body: {
label: 'body',
fields: [
{ key: 'body',
editor: 'text',
type: 'string',
localized: true,
markup: true,
},
],
},
visual: {
label: 'visual',
fields: [
{ key: 'visual',
editor: 'file',
accept: [ 'image/*' ],
},
],
},
updates: {
label: 'updates',
fields: [
{ key: 'updates',
editor: 'notes',
userId: true,
placeholder: 'updates',
},
],
},
notes: {
label: 'notes',
fields: [
{ key: 'notes',
editor: 'notes',
userId: true,
//localized: true,
placeholder: 'notes',
},
],
},
},
},
}),
schema: () => ({
id: {
type: 'string',
required: ({ newEntity }) => newEntity,
lowercase: true,
trim: true,
filter: {
title: 'id',
match: '^[a-zA-Z0-9]{6}$',
order: 999,
},
default: () => makeId(6),
validate: async ({ newValues, oldValues, newEntity, storage }) => {
if (newEntity) {
const r = storage ? await storage.store('webdesq/blog').get(newValues.id) : null;
return (r == null ? true : 'id already exists');
}
else {
// ID cannot be changed if record was already created
return (newValues.id == oldValues.id ? true : 'id cannot be changed');
}
},
},
name: {
type: 'stringset',
trim: true,
required: [ true, 'Name is missing!' ],
filter: {
title: 'title',
match: '^[a-zA-Z0-9]*',
localized: true,
},
format: {
label: 'name',
type: 'text',
sortable: 'string',
align: 'left',
minWidth: 150,
enabled: true,
priority: 1,
},
},
state: {
type: 'string',
required: true,
default: 'concept',
filter: {
title: 'state',
options: states,
},
format: {
label: 'state',
type: 'state',
align: 'left',
sortable: 'text',
sorted: 'down',
minWidth: 90,
enabled: true,
options: {
concept: {
name: 'concept',
class: 'l2',
},
online: {
name: 'online',
class: 'l4',
},
offline: {
name: 'offline',
class: 'l1',
},
},
},
},
severity: {
type: 'string',
required: true,
default: '',
filter: {
title: 'severity',
options: severity,
},
format: {
label: 'severity',
type: 'state',
align: 'left',
sortable: 'text',
minWidth: 90,
enabled: true,
options: {
major: {
name: 'major',
class: 'l1',
},
minor: {
name: 'minor',
class: 'l2',
},
scheduled: {
name: 'scheduled',
class: 'l3',
},
none: {
name: 'no impact',
class: 'l4',
},
},
},
},
resolved: {
type: 'boolean',
default: true,
format: {
type: 'checkbox',
sortable: 'boolean',
align: 'center',
label: 'resolved?',
minWidth: 90,
enabled: true,
//type: 'state',
//options: {
// true: {
// name: 'yes',
// class: 'l5',
// },
// false: {
// name: 'no',
// class: 'l1',
// },
//},
},
},
tags: {
type: 'array',
of: 'string',
default: [],
filter: {
title: 'tags',
match: '^[a-z]',
options: async ({ storage }) => (await storage.store('smartyellow/webserviceoutage').find({ 'log.deleted': { $exists: false } }, { keys: [ 'id', 'tags' ] }).sort({ name: 1 }).toArray())
.reduce((acc, curr) => {
if (curr.tags) {
for (let i = 0; i < curr.tags.length; i++) {
acc[curr.tags[i]] = curr.tags[i];
}
}
return acc;
}, {}),
},
format: {
type: 'text',
label: 'tags',
sortable: 'text',
enabled: true,
minWidth: 100,
},
},
summary: {
type: 'stringset',
default: '',
},
body: {
type: 'stringset',
default: '',
filter: {
title: 'message contains',
match: '[a-z0-9A-Z]*',
localized: true,
},
},
visual: {
type: 'array',
of: [ 'string' ],
default: [],
skip: true,
onDataValid: async ({ newValues, storage, user }) => {
newValues.visual = newValues.visual || [];
for (let i = 0; i < newValues.visual.length; i++) {
if (newValues.visual[i].data) {
if (storage) {
// If storage is available, insert the new file into storage and collect id
const result = await storage({ user }).bucket('webdesq/media').insert({
id: makeId(6),
filename: newValues.visual[i].name,
metadata: {
contentType: newValues.visual[i].type,
},
}, newValues.visual[i].data)
.catch(error => {
if (error.code !== 'DUPLICATE_FILE') {
throw error;
}
newValues.visual[i] = error.file.id;
});
if (result) {
newValues.visual[i] = result.id;
}
}
else {
// If no storage is available, remove slot by setting it to null
newValues.visual[i] = null;
}
}
// remove empty slots in photo array
newValues.visual = newValues.visual.filter(i => i != null);
newValues.visual = [ ...new Set(newValues.visual) ];
}
},
},
notes: {
type: 'array',
of: {},
default: [],
},
updates: {
type: 'array',
of: {},
default: [],
},
}),
};