agendash 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/Dockerfile +3 -2
  2. package/History.md +80 -58
  3. package/LICENSE +1 -1
  4. package/README.md +173 -49
  5. package/all-jobs.png +0 -0
  6. package/app.js +12 -7
  7. package/bin/agendash-standalone-fastify.js +61 -0
  8. package/bin/agendash-standalone-hapi.js +60 -0
  9. package/bin/agendash-standalone-koa.js +56 -0
  10. package/bin/agendash-standalone.js +47 -15
  11. package/create-job.png +0 -0
  12. package/entrypoint.sh +1 -1
  13. package/lib/controllers/agendash.js +340 -0
  14. package/lib/middlewares/README.md +12 -5
  15. package/lib/middlewares/express.js +58 -34
  16. package/lib/middlewares/fastify.js +75 -0
  17. package/lib/middlewares/hapi.js +65 -88
  18. package/lib/middlewares/koa.js +71 -0
  19. package/mobile-ui-sm.png +0 -0
  20. package/mobile-ui-xs.png +0 -0
  21. package/package.json +45 -36
  22. package/public/android-chrome-192x192.png +0 -0
  23. package/public/android-chrome-512x512.png +0 -0
  24. package/public/app/assets/ArgensdashV2Logo.svg +18 -0
  25. package/public/app/css/styles.css +221 -0
  26. package/public/app/js/confirmDelete.js +40 -0
  27. package/public/app/js/confirmDeleteMulti.js +45 -0
  28. package/public/app/js/confirmRequeue.js +40 -0
  29. package/public/app/js/confirmRequeueMulti.js +45 -0
  30. package/public/app/js/confirms.js +8 -0
  31. package/public/app/js/jobdetail.js +48 -0
  32. package/public/app/js/joblist.js +239 -0
  33. package/public/app/js/main.js +275 -0
  34. package/public/app/js/newJob.js +89 -0
  35. package/public/app/js/sidebar.js +121 -0
  36. package/public/app/js/topbar.js +95 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/favicon-16x16.png +0 -0
  39. package/public/favicon-32x32.png +0 -0
  40. package/public/favicon.ico +0 -0
  41. package/public/index.html +40 -164
  42. package/public/site.webmanifest +1 -0
  43. package/search.png +0 -0
  44. package/.editorconfig +0 -7
  45. package/.travis.yml +0 -24
  46. package/job-details.png +0 -0
  47. package/lib/agendash.js +0 -295
  48. package/public/css/bootstrap-theme.css +0 -587
  49. package/public/css/bootstrap-theme.css.map +0 -1
  50. package/public/css/bootstrap-theme.min.css +0 -6
  51. package/public/css/bootstrap-theme.min.css.map +0 -1
  52. package/public/css/bootstrap.css +0 -6760
  53. package/public/css/bootstrap.css.map +0 -1
  54. package/public/css/bootstrap.min.css +0 -6
  55. package/public/css/bootstrap.min.css.map +0 -1
  56. package/public/css/dashboard.css +0 -94
  57. package/public/fonts/glyphicons-halflings-regular.eot +0 -0
  58. package/public/fonts/glyphicons-halflings-regular.svg +0 -288
  59. package/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  60. package/public/fonts/glyphicons-halflings-regular.woff +0 -0
  61. package/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  62. package/public/js/backbone-min.js +0 -2
  63. package/public/js/backbone-min.map +0 -1
  64. package/public/js/bootstrap.js +0 -2363
  65. package/public/js/bootstrap.min.js +0 -7
  66. package/public/js/jquery-2.2.0.min.js +0 -4
  67. package/public/js/lodash.min.js +0 -120
  68. package/public/js/main.js +0 -414
  69. package/public/js/moment.min.js +0 -7
  70. package/test/test-express.js +0 -98
  71. package/test/test-hapi.js +0 -104
  72. package/yarn.lock +0 -4412
@@ -0,0 +1,75 @@
1
+ const path = require("path");
2
+
3
+ module.exports = (agendash) => (instance, opts, done) => {
4
+ const { api, requeueJobs, deleteJobs, createJob } = agendash;
5
+
6
+ instance.register(require("fastify-static"), {
7
+ root: path.join(__dirname, "../../public"),
8
+ });
9
+
10
+ instance.get("/", function (req, reply) {
11
+ return reply.sendFile("index.html");
12
+ });
13
+
14
+ instance.get("/api", async (request, response) => {
15
+ try {
16
+ const {
17
+ job,
18
+ state,
19
+ skip,
20
+ limit,
21
+ q,
22
+ property,
23
+ isObjectId,
24
+ } = request.query;
25
+ const apiResponse = await api(job, state, {
26
+ query: q,
27
+ property,
28
+ isObjectId,
29
+ skip,
30
+ limit,
31
+ });
32
+ response.send(apiResponse);
33
+ } catch (error) {
34
+ response.status(400).send(error);
35
+ }
36
+ });
37
+
38
+ instance.post("/api/jobs/requeue", async (request, response) => {
39
+ try {
40
+ const newJobs = await requeueJobs(request.body.jobIds);
41
+ response.send(newJobs);
42
+ } catch (error) {
43
+ response.status(404).send(error);
44
+ }
45
+ });
46
+
47
+ instance.post("/api/jobs/delete", async (request, response) => {
48
+ try {
49
+ const deleted = await deleteJobs(request.body.jobIds);
50
+ if (deleted) {
51
+ response.send({ deleted: true });
52
+ } else {
53
+ response.send({ message: "Jobs not deleted" });
54
+ }
55
+ } catch (error) {
56
+ response.status(404).send(error);
57
+ }
58
+ });
59
+
60
+ instance.post("/api/jobs/create", async (request, response) => {
61
+ try {
62
+ await createJob(
63
+ request.body.jobName,
64
+ request.body.jobSchedule,
65
+ request.body.jobRepeatEvery,
66
+ request.body.jobData
67
+ );
68
+ response.send({ created: true });
69
+ } catch (error) {
70
+ response.status(400).send(error);
71
+ }
72
+ });
73
+
74
+ done();
75
+ };
@@ -1,118 +1,95 @@
1
- 'use strict';
1
+ const path = require("path");
2
+ const pack = require("../../package.json");
2
3
 
3
- const path = require('path');
4
- const pack = require('../../package.json');
5
-
6
- module.exports = agendash => {
4
+ module.exports = (agendash) => {
5
+ const { api, requeueJobs, deleteJobs, createJob } = agendash;
7
6
  return {
8
7
  pkg: pack,
9
- register: async (server, options) => {
8
+ register: (server, options) => {
10
9
  server.route([
11
10
  {
12
- method: 'GET',
13
- path: '/{param*}',
11
+ method: "GET",
12
+ path: "/{param*}",
14
13
  handler: {
15
14
  directory: {
16
- path: path.join(__dirname, '../../public')
17
- }
15
+ path: path.join(__dirname, "../../public"),
16
+ },
18
17
  },
19
18
  config: {
20
- auth: options.auth || false
21
- }
19
+ auth: options.auth || false,
20
+ },
22
21
  },
23
22
  {
24
- method: 'GET',
25
- path: '/api',
26
- handler: async (request, h) => {
27
- try {
28
- const apiResponse = await new Promise((resolve, reject) => {
29
- agendash.api(request.query.job, request.query.state, (err, apiResponse) => {
30
- if (err) {
31
- return reject(err);
32
- }
33
- resolve(apiResponse);
34
- });
35
- });
36
- return h.response(apiResponse);
37
- } catch (err) {
38
- return h.response(err).code(400);
39
- }
23
+ method: "GET",
24
+ path: "/api",
25
+ handler(request) {
26
+ const {
27
+ job,
28
+ state,
29
+ limit,
30
+ skip,
31
+ q,
32
+ property,
33
+ isObjectId,
34
+ } = request.query;
35
+ return api(job, state, {
36
+ query: q,
37
+ property,
38
+ isObjectId,
39
+ skip,
40
+ limit,
41
+ });
40
42
  },
41
43
  config: {
42
- auth: options.auth || false
43
- }
44
+ auth: options.auth || false,
45
+ },
44
46
  },
45
47
  {
46
- method: 'POST',
47
- path: '/api/jobs/requeue',
48
- handler: async (request, h) => {
49
- try {
50
- const requeueJobs = await new Promise((resolve, reject) => {
51
- agendash.requeueJobs(request.payload.jobIds, (err, requeueJobs) => {
52
- if (err) {
53
- return reject(err);
54
- }
55
- resolve(requeueJobs);
56
- });
57
- });
58
- return h.response(requeueJobs);
59
- } catch (err) {
60
- return h.response(err).code(404);
61
- }
48
+ method: "POST",
49
+ path: "/api/jobs/requeue",
50
+ handler(request) {
51
+ return requeueJobs(request.payload.jobIds);
62
52
  },
63
53
  config: {
64
- auth: options.auth || false
65
- }
54
+ auth: options.auth || false,
55
+ },
66
56
  },
67
57
  {
68
- method: 'POST',
69
- path: '/api/jobs/delete',
70
- handler: async (request, h) => {
71
- try {
72
- const deletedJobs = await new Promise((resolve, reject) => {
73
- agendash.deleteJobs(request.payload.jobIds, err => {
74
- if (err) {
75
- return reject(err);
76
- }
77
- resolve({
78
- deleted: true
79
- });
80
- });
81
- });
82
- return h.response(deletedJobs);
83
- } catch (err) {
84
- return h.response(err).code(404);
58
+ method: "POST",
59
+ path: "/api/jobs/delete",
60
+ async handler(request, h) {
61
+ const deleted = await deleteJobs(request.payload.jobIds);
62
+ if (deleted) {
63
+ return h.response({ deleted: true });
85
64
  }
65
+
66
+ return h.code(404);
86
67
  },
87
68
  config: {
88
- auth: options.auth || false
89
- }
69
+ auth: options.auth || false,
70
+ },
90
71
  },
91
72
  {
92
- method: 'POST',
93
- path: '/api/jobs/create',
94
- handler: async (request, h) => {
95
- try {
96
- const createdJobs = await new Promise((resolve, reject) => {
97
- agendash.createJob(request.payload.jobName, request.payload.jobSchedule, request.payload.jobRepeatEvery, request.payload.jobData, err => {
98
- if (err) {
99
- return reject(err);
100
- }
101
- resolve({
102
- created: true
103
- });
104
- });
105
- });
106
- return h.response(createdJobs);
107
- } catch (err) {
108
- return h.response(err).code(404);
73
+ method: "POST",
74
+ path: "/api/jobs/create",
75
+ async handler(request, h) {
76
+ const created = await createJob(
77
+ request.payload.jobName,
78
+ request.payload.jobSchedule,
79
+ request.payload.jobRepeatEvery,
80
+ request.payload.jobData
81
+ );
82
+ if (created) {
83
+ return h.response({ created: true });
109
84
  }
85
+
86
+ return h.code(404);
110
87
  },
111
88
  config: {
112
- auth: options.auth || false
113
- }
114
- }
89
+ auth: options.auth || false,
90
+ },
91
+ },
115
92
  ]);
116
- }
93
+ },
117
94
  };
118
95
  };
@@ -0,0 +1,71 @@
1
+ const path = require("path");
2
+ const bodyParser = require("koa-bodyparser");
3
+ const Router = require("koa-router");
4
+ const koaStatic = require("koa-static");
5
+
6
+ module.exports = (agendash) => {
7
+ const middlewares = [];
8
+
9
+ middlewares.push(
10
+ koaStatic(path.resolve(__dirname, "../../public"), { defer: true })
11
+ );
12
+
13
+ middlewares.push(bodyParser());
14
+
15
+ const routerApi = new Router();
16
+
17
+ routerApi.get("/api", async ({ response, query }) => {
18
+ const { job, state, skip, limit, q, property, isObjectId } = query;
19
+ try {
20
+ response.body = await agendash.api(job, state, {
21
+ query: q,
22
+ property,
23
+ isObjectId,
24
+ skip,
25
+ limit,
26
+ });
27
+ } catch (error) {
28
+ response.status = 400;
29
+ response.body = error;
30
+ }
31
+ });
32
+
33
+ routerApi.post("/api/jobs/requeue", async ({ request, response }) => {
34
+ try {
35
+ response.body = await agendash.requeueJobs(request.body.jobIds);
36
+ } catch (error) {
37
+ response.status = 404;
38
+ response.body = error;
39
+ }
40
+ });
41
+
42
+ routerApi.post("/api/jobs/delete", async ({ request, response }) => {
43
+ try {
44
+ await agendash.deleteJobs(request.body.jobIds);
45
+ response.body = {
46
+ deleted: true,
47
+ };
48
+ } catch (error) {
49
+ response.status = 404;
50
+ response.body = error;
51
+ }
52
+ });
53
+
54
+ routerApi.post("/api/jobs/create", async ({ request, response }) => {
55
+ const { jobData, jobName, jobSchedule, jobRepeatEvery } = request.body;
56
+
57
+ try {
58
+ await agendash.createJob(jobName, jobSchedule, jobRepeatEvery, jobData);
59
+ response.body = {
60
+ created: true,
61
+ };
62
+ } catch (error) {
63
+ response.status = 404;
64
+ response.body = error;
65
+ }
66
+ });
67
+
68
+ middlewares.push(routerApi.middleware());
69
+
70
+ return middlewares;
71
+ };
Binary file
Binary file
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "agendash",
3
- "version": "1.0.0",
4
- "description": "Agenda Dashboard",
3
+ "version": "3.0.0",
4
+ "description": "A modern dashboard for Agenda.js with Pagination and Search capabilities",
5
5
  "main": "app.js",
6
6
  "bin": "bin/agendash-standalone.js",
7
7
  "engines": {
8
- "node": ">=8.9.0"
8
+ "node": ">=10.0.0"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "run-p lint ava",
12
- "lint": "xo",
12
+ "lint": "eslint ./",
13
13
  "ava": "ava -v -c 1"
14
14
  },
15
15
  "keywords": [
16
- "agenda"
16
+ "agenda",
17
+ "agendash",
18
+ "dashboard",
19
+ "job-queues"
17
20
  ],
18
21
  "repository": {
19
22
  "type": "git",
@@ -24,43 +27,49 @@
24
27
  },
25
28
  "homepage": "https://github.com/agenda/agendash#readme",
26
29
  "license": "MIT",
30
+ "files": [
31
+ "bin",
32
+ "lib",
33
+ "public",
34
+ "app.js",
35
+ "*.png",
36
+ "Dockerfile",
37
+ "entrypoint.sh"
38
+ ],
27
39
  "dependencies": {
28
- "agenda": "^2.0.0",
29
- "async": "^2.6.0",
40
+ "agenda": "^4.2.1",
30
41
  "body-parser": "^1.15.0",
31
42
  "commander": "^2.9.0",
32
43
  "express": "^4.0.0",
33
- "hapi": "^17.5.1",
34
- "inert": "^5.1.0",
35
- "semver": "^5.3.0"
44
+ "mongodb": "*",
45
+ "semver": "^7.3.4"
36
46
  },
37
47
  "devDependencies": {
38
- "ava": "^0.25.0",
39
- "npm-run-all": "^4.1.2",
40
- "supertest": "^3.0.0",
41
- "xo": "^0.18.2"
48
+ "@hapi/hapi": "^20.2.1",
49
+ "@hapi/inert": "^6.0.5",
50
+ "ava": "3.15.0",
51
+ "eslint": "7.19.0",
52
+ "fastify": "^3.24.0",
53
+ "fastify-static": "^4.5.0",
54
+ "koa": "2.13.1",
55
+ "koa-bodyparser": "4.3.0",
56
+ "koa-router": "10.0.0",
57
+ "koa-static": "5.0.0",
58
+ "npm-run-all": "4.1.5",
59
+ "prettier": "2.2.1",
60
+ "supertest": "^6.2.2"
42
61
  },
43
- "xo": {
44
- "space": 2,
45
- "rules": {
46
- "space-before-function-paren": [
47
- "error",
48
- "never"
49
- ],
50
- "max-params": [
51
- "error",
52
- 5
53
- ],
54
- "max-nested-callbacks": [
55
- "error",
56
- 5
57
- ]
62
+ "eslintConfig": {
63
+ "parserOptions": {
64
+ "ecmaVersion": 2019
65
+ },
66
+ "env": {
67
+ "es6": true,
68
+ "node": true
58
69
  },
59
- "envs": [
60
- "node"
61
- ],
62
- "ignores": [
63
- "public/**/*"
64
- ]
65
- }
70
+ "extends": "eslint:recommended"
71
+ },
72
+ "eslintIgnore": [
73
+ "public"
74
+ ]
66
75
  }
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+ viewBox="0 0 468.9 334.3" style="enable-background:new 0 0 468.9 334.3;" xml:space="preserve">
5
+ <style type="text/css">
6
+ .st0{fill:#36A9E1;}
7
+ .st1{fill:#FFFFFF;}
8
+ .st2{fill:#FFCF02;}
9
+ </style>
10
+ <rect x="61.8" y="155.8" class="st0" width="46.6" height="159.2"/>
11
+ <rect x="117.4" y="124.8" class="st0" width="46.6" height="190.2"/>
12
+ <rect x="179.4" y="107.1" class="st1" width="46.6" height="207.9"/>
13
+ <rect x="239.4" y="89.4" class="st1" width="46.6" height="225.6"/>
14
+ <rect x="296.3" y="53.8" class="st0" width="46.6" height="261.2"/>
15
+ <rect x="351.9" y="16.3" class="st0" width="46.6" height="298.7"/>
16
+ <path class="st2" d="M178.8,241c9.4,16.8,26.6,28.8,46.6,31V150.1c-20.1,2.2-37.2,14.2-46.6,31V241z"/>
17
+ <path class="st2" d="M238.8,150.1V272c20-2.1,37.1-13.8,46.6-30.4v-61.2C275.9,163.9,258.8,152.2,238.8,150.1z"/>
18
+ </svg>
@@ -0,0 +1,221 @@
1
+ :root{
2
+ --secondary: #ffffff;
3
+ --primary: #2b2b2b;
4
+ --warning: rgb(255, 94, 0);
5
+ --pill-own-time: #00b7ff;
6
+ }
7
+ * {
8
+ font-family: 'Roboto', sans-serif;
9
+ }
10
+ .tittle {
11
+ font-family: 'Days One', sans-serif;
12
+ font-size: 22px;
13
+ }
14
+ .logo{
15
+ margin-bottom: 12px;
16
+ }
17
+
18
+ /* width */
19
+ ::-webkit-scrollbar {
20
+ width: 10px;
21
+ }
22
+
23
+ /* Track */
24
+ ::-webkit-scrollbar-track {
25
+ background-color: rgb(202, 202, 202);
26
+ box-shadow: inset 0 0 5px rgb(211, 211, 211);
27
+ border-radius: 10px;
28
+ }
29
+
30
+ /* Handle */
31
+ ::-webkit-scrollbar-thumb {
32
+ background: rgb(65, 65, 65);
33
+ border-radius: 10px;
34
+ }
35
+
36
+ /* Handle on hover */
37
+ ::-webkit-scrollbar-thumb:hover {
38
+ background: #000000;
39
+ }
40
+
41
+ body {
42
+ overflow-y: hidden;
43
+ }
44
+
45
+ .sidebar, main {
46
+ max-height: calc(100vh - 45px);
47
+ overflow-y: scroll;
48
+ }
49
+
50
+ .joblist {
51
+ overflow-y: scroll;
52
+ }
53
+ .sortable{
54
+ float: right
55
+ }
56
+ .sortableinactive{
57
+ float: right;
58
+ opacity: 0.2;
59
+ }
60
+ .table-headers, .table-row {
61
+ display: flex;
62
+ justify-content: space-between;
63
+ }
64
+
65
+ .popupmessage{
66
+ width: 60vw;
67
+ position: absolute;
68
+ top: 30px;
69
+ right: 10px;
70
+ z-index: 999999;
71
+ }
72
+ code {
73
+ padding: 5px;
74
+ background-color: #eeeeee;
75
+ }
76
+ .action-btn{
77
+ margin: 0px;
78
+ padding: 1px 2px;
79
+ cursor: pointer;
80
+ }
81
+ .action-btn:hover{
82
+ color: var(--warning-secondary);
83
+ }
84
+ .oi{
85
+ top:0px !important;
86
+ }
87
+ .pill-own {
88
+ padding: 3px 3px;
89
+ font-size: 10px;
90
+ color: var(--secondary);
91
+ border-radius: 3px;
92
+ }
93
+ .pill-big-own {
94
+ padding: 2px 2px;
95
+ font-size: 13px;
96
+ max-height: 23px;
97
+ color: var(--secondary);
98
+ border-radius: 3px;
99
+ }
100
+ .progress{
101
+ height: 5px;
102
+ }
103
+ .md-custom {
104
+ font-size: 21px;
105
+ -webkit-font-smoothing: antialiased;
106
+ -moz-osx-font-smoothing: grayscale;
107
+ }
108
+ .mybtn{
109
+ cursor: pointer;
110
+ }
111
+ .mybtn:hover{
112
+ background-color: rgb(218, 218, 218);
113
+ }
114
+
115
+ .pill-own span {
116
+ line-height: 12px;
117
+ font-size: 10px;
118
+ font-style: normal;
119
+ }
120
+ .IcoInButton{
121
+ font-size: 13px;
122
+ margin: 0 2px;
123
+ }
124
+
125
+ .json-editor {
126
+ height: 150px;
127
+ }
128
+ .card-responsive-title-container {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ }
133
+ .card-responsive-name {
134
+ font-weight: 500;
135
+ line-height: 1.2;
136
+ font-size: 1.25rem;
137
+ }
138
+ .card-responsive-status-title {
139
+ color: grey;
140
+ font-size: 14px;
141
+ text-align: center;
142
+ }
143
+ .card-responsive-checkbox {
144
+ width: 22px;
145
+ height: 22px;
146
+ }
147
+ .material-icons-size {
148
+ font-size: 25px;
149
+ }
150
+ .pill-own-card {
151
+ display: flex;
152
+ align-items: center;
153
+ padding: 6px 10px
154
+ }
155
+ .pill-own-card-info {
156
+ font-size: 12px!important;
157
+ text-align: center;
158
+ }
159
+ @media (min-width: 768px) {
160
+ .col-md-10 {
161
+ flex: 0 0 70%;
162
+ max-width: 70%;
163
+ }
164
+ .col-md-2 {
165
+ flex: 0 0 30%;
166
+ max-width: 30%;
167
+ }
168
+ }
169
+ @media (min-width: 1150px) {
170
+ .col-md-10 {
171
+ flex: 0 0 80%;
172
+ max-width: 80%;
173
+ }
174
+ .col-md-2 {
175
+ flex: 0 0 20%;
176
+ max-width: 20%;
177
+ }
178
+ }
179
+
180
+ /* The sidebar menu */
181
+ .sidebar-collapse {
182
+ height: 100%;
183
+ width: 0;
184
+ position: fixed;
185
+ z-index: 1;
186
+ top: 0;
187
+ left: 0;
188
+ background-color: white;
189
+ padding-top: 45px;
190
+ transition: 0.5s;
191
+ }
192
+ .sidebar-collapse a {
193
+ padding: 8px 8px 8px 32px;
194
+ text-decoration: none;
195
+ font-size: 25px;
196
+ color: #818181;
197
+ display: block;
198
+ transition: 0.3s;
199
+ }
200
+ .sidebar-collapse a:hover {
201
+ color: #f1f1f1;
202
+ }
203
+ .sidebar-collapse .closebtn {
204
+ position: absolute;
205
+ top: -10px;
206
+ right: 25px;
207
+ font-size: 36px;
208
+ margin-left: 50px;
209
+ }
210
+ .slidebar-container-button {
211
+ position: absolute;
212
+ left: 85%;
213
+ }
214
+ .openbtn {
215
+ font-size: 23px;
216
+ cursor: pointer;
217
+ background-color: #343a40;
218
+ color: white;
219
+ padding: 5px 0 0 0;
220
+ border: none;
221
+ }