├── .eslintignore
├── .gitignore
├── Procfile
├── assets
├── task_button.png
├── hotkeys_button.png
└── web-ui-screen.png
├── client
├── assets
│ ├── logo.png
│ ├── logo.psd
│ ├── logo2.png
│ ├── logo2_small.png
│ └── logo2_transparent.png
├── helpers
│ ├── tabs.js
│ ├── hotKeys.js
│ ├── WindowAttached.js
│ ├── taskList.js
│ └── dom_utils.js
├── scripts
│ ├── Api.js
│ ├── ThemeSwitcher.js
│ ├── WebSocket.js
│ ├── Configs.js
│ ├── Tabs.js
│ ├── HotKeys.js
│ ├── WebLogger.js
│ └── TaskList.js
├── styles
│ ├── fullscreen.css
│ ├── hot_keys-shortcuts.css
│ └── dark-theme.css
├── small-screens.css
├── medium-screens.css
├── controllers
│ └── Header.js
├── index.html
├── global.js
└── styles.css
├── nodemon.json
├── server
├── utils
│ ├── emitter.js
│ ├── mem_cache.js
│ ├── colors.js
│ ├── envs.js
│ ├── commands.js
│ └── processes.js
├── constants.js
├── ws.js
├── processWorker.js
├── config.js
├── taskfile.js
└── server.js
├── postinstall.js
├── .vscode
└── launch.json
├── .eslintrc.yml
├── babel.config.js
├── LICENSE
├── webpack.config.js
├── .travis.yml
├── package.json
├── flamebird.js
├── README.md
└── CHANGELOG.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist
3 | .flamebirdrc
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | client: nodemon --exec \"npm run client:dev\" --watch client -V
2 |
--------------------------------------------------------------------------------
/assets/task_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/assets/task_button.png
--------------------------------------------------------------------------------
/client/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/client/assets/logo.png
--------------------------------------------------------------------------------
/client/assets/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/client/assets/logo.psd
--------------------------------------------------------------------------------
/assets/hotkeys_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/assets/hotkeys_button.png
--------------------------------------------------------------------------------
/assets/web-ui-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/assets/web-ui-screen.png
--------------------------------------------------------------------------------
/client/assets/logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/client/assets/logo2.png
--------------------------------------------------------------------------------
/client/assets/logo2_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/client/assets/logo2_small.png
--------------------------------------------------------------------------------
/client/assets/logo2_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acacode/flamebird/HEAD/client/assets/logo2_transparent.png
--------------------------------------------------------------------------------
/client/helpers/tabs.js:
--------------------------------------------------------------------------------
1 | export const createTab = (name, isActive) => ({
2 | name,
3 | active: !!isActive,
4 | })
5 |
--------------------------------------------------------------------------------
/client/helpers/hotKeys.js:
--------------------------------------------------------------------------------
1 | export const clearifyEvent = (e = window.event) => {
2 | e.preventDefault()
3 | e.stopPropagation()
4 | }
5 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": false,
3 | "ext": "js,css,html",
4 | "delay": "3000",
5 | "watch": ["client/**/*", "server/**/*", "flamebird.js", "webpack.config.js"],
6 | "ignore": ["node_modules"]
7 | }
--------------------------------------------------------------------------------
/client/helpers/WindowAttached.js:
--------------------------------------------------------------------------------
1 | export default function WindowAttached(windowProperty) {
2 | return class WindowAttached {
3 | constructor() {
4 | window[windowProperty] = this
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/server/utils/emitter.js:
--------------------------------------------------------------------------------
1 | const events = require('events')
2 |
3 | const emitter = new events.EventEmitter()
4 |
5 | emitter.once('killall', signal =>
6 | console.log('Killing all processes with signal ', signal)
7 | )
8 |
9 | emitter.setMaxListeners(50)
10 |
11 | module.exports = emitter
12 |
--------------------------------------------------------------------------------
/postinstall.js:
--------------------------------------------------------------------------------
1 | const { yellow } = require('./server/utils/colors')
2 |
3 | const strings = []
4 | strings.push(yellow('╔═════════════════════════════════════╗'))
5 | strings.push(yellow('║ Thanks for using Flamebird.js ║'))
6 | strings.push(yellow('╚═════════════════════════════════════╝'))
7 | console.log('\r\n' + strings.join('\r\n') + '\r\n')
8 |
--------------------------------------------------------------------------------
/server/utils/mem_cache.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 |
3 | const memCache = {}
4 |
5 | const getCache = (key, defaultValue) =>
6 | _.isUndefined(memCache[key]) ? defaultValue || null : memCache[key]
7 |
8 | const setToCache = (key, value) => (memCache[key] = value)
9 |
10 | module.exports = {
11 | get: getCache,
12 | set: setToCache,
13 | }
14 |
--------------------------------------------------------------------------------
/server/utils/colors.js:
--------------------------------------------------------------------------------
1 | const specChar = char => '\x1b[' + char + 'm'
2 |
3 | const colorModifier = (colorCode, isFg = false) => str =>
4 | specChar(colorCode) + str + specChar(isFg ? 39 : 49)
5 |
6 | module.exports = {
7 | yellow: colorModifier(33),
8 | red: colorModifier(31),
9 | grey: colorModifier(90),
10 | blue: colorModifier(34),
11 | cyan: colorModifier(36),
12 | magentaFg: colorModifier(45, true),
13 | }
14 |
--------------------------------------------------------------------------------
/server/constants.js:
--------------------------------------------------------------------------------
1 | const MESSAGE_TYPES = {
2 | CONNECTION: 'CONNECTION',
3 | LOG: 'LOG',
4 | APPS_LIST_UPDATE: 'APPS_LIST_UPDATE',
5 | }
6 |
7 | const PATHS = {
8 | // paths will been resolved via path.resolve(__dirname)
9 | WEB_APP_ROOT: '../dist',
10 | }
11 |
12 | const TASK_RUNNERS = {
13 | NPM: 'npm run',
14 | YARN: 'yarn run',
15 | }
16 |
17 | module.exports = {
18 | MESSAGE_TYPES,
19 | PATHS,
20 | TASK_RUNNERS,
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "attach",
10 | "name": "Attach by Process ID",
11 | "processId": "${command:PickProcess}"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - standard
3 | - prettier
4 | - prettier/standard
5 |
6 | parser: babel-eslint
7 |
8 | plugins:
9 | - standard
10 | - prettier
11 |
12 | env:
13 | browser: true
14 |
15 | rules:
16 | # no-undef: 0
17 | prettier/prettier:
18 | - error
19 | - trailingComma: es5
20 | singleQuote: true
21 | semi: false
22 | no-var: 2
23 | prefer-const: 2
24 |
25 | # TODO: The following rules should be removed incrementally in order to fully
26 | # integrate lint rules from the Standard ESLint config and Prettier
27 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 |
4 | module.exports = {
5 | "sourceMaps": true,
6 | "presets": [
7 | [
8 | "@babel/preset-env",
9 | {
10 | "targets": {
11 | "browsers": ["last 2 versions"]
12 | },
13 | "modules": "commonjs"
14 | },
15 | ]
16 | ],
17 | "ignore": [/[\/\\]core-js/, /@babel[\/\\]runtime/],
18 | "plugins": [
19 | "@babel/plugin-transform-runtime",
20 | "@babel/plugin-syntax-dynamic-import",
21 | "@babel/plugin-proposal-class-properties",
22 | "@babel/plugin-proposal-export-default-from",
23 | "@babel/plugin-proposal-export-namespace-from",
24 | "@babel/plugin-proposal-object-rest-spread"
25 | ]
26 | }
--------------------------------------------------------------------------------
/client/scripts/Api.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import kinka from 'kinka'
3 |
4 | export default new (class Api {
5 | getProjectInfo = () => kinka.get('/info')
6 | getProjectVersion = () => kinka.get('/project-version')
7 | runTask = (configId, taskId) => kinka.post(`/${configId}/${taskId}/run`)
8 | stopTask = (configId, taskId) => kinka.post(`/${configId}/${taskId}/stop`)
9 | clearLogs = (configId, taskId) => kinka.delete(`/${configId}/${taskId}/logs`)
10 | getLogs = (configId, taskId) => kinka.get(`/${configId}/${taskId}/logs`)
11 | removeConfig = configId => kinka.delete(`/${configId}`)
12 | updateEnvs = (configId, taskId, envs) =>
13 | kinka.post(`/${configId}/${taskId}/envs`, _.clone(envs))
14 | })()
15 |
--------------------------------------------------------------------------------
/client/scripts/ThemeSwitcher.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import WindowAttached from '../helpers/WindowAttached'
3 |
4 | export const THEMES = {
5 | WHITE: 'white',
6 | DARK: 'dark',
7 | }
8 |
9 | export default class ThemeSwitcher extends WindowAttached('ThemeSwitcher') {
10 | theme = THEMES.WHITE
11 |
12 | onSwitchTheme = _.noop
13 |
14 | setTheme = newTheme => {
15 | localStorage.setItem('theme', newTheme)
16 | document.body.setAttribute('theme', newTheme)
17 | this.theme = newTheme
18 |
19 | this.onSwitchTheme(this.theme)
20 | }
21 |
22 | switchTheme = () => {
23 | const newTheme = this.theme === THEMES.DARK ? THEMES.WHITE : THEMES.DARK
24 | this.setTheme(newTheme)
25 | }
26 |
27 | constructor({ onSwitchTheme }) {
28 | super()
29 | this.onSwitchTheme = onSwitchTheme
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/styles/fullscreen.css:
--------------------------------------------------------------------------------
1 | @media screen and (min-width: 923px) {
2 | body[fullscreen="true"] {
3 | padding: 0.9% 0;
4 | }
5 |
6 | [fullscreen="true"] .wrapper {
7 | max-width: calc(98.2% - 40px);
8 | }
9 |
10 | [fullscreen="true"] header .title {
11 | padding-left: 49px;
12 | }
13 |
14 | [fullscreen="true"] #task-list,[fullscreen="true"] .task.new-task {
15 | width: 260px;
16 | }
17 |
18 | [fullscreen="true"] #task-logs {
19 | width: calc(100% - 260px);
20 | }
21 | [fullscreen="true"] .scrolling > .task-data {
22 | top: 48px;
23 | }
24 |
25 | [fullscreen="true"] .logo {
26 | left: 3px;
27 | z-index: 20;
28 | top: 4px;
29 | }
30 |
31 | [fullscreen="true"] .logs-container {
32 | padding: 20px 20px 27px;
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/client/scripts/WebSocket.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | const { MESSAGE_TYPES } = require('../../server/constants')
3 |
4 | export default class WebSocket {
5 | connection = null
6 |
7 | constructor(url, { onConnection, onLogUpdate, onAppListUpdate } = {}) {
8 | this.onConnection = onConnection || _.noop
9 | this.onLogUpdate = onLogUpdate || _.noop
10 | this.onAppListUpdate = onAppListUpdate || _.noop
11 |
12 | this.connection = new window.WebSocket(url)
13 | this.connection.onmessage = this.handleMessageReceive
14 | }
15 |
16 | handleMessageReceive = ({ data }) => {
17 | const { type, message } = JSON.parse(data)
18 |
19 | switch (type) {
20 | case MESSAGE_TYPES.CONNECTION:
21 | return this.onConnection(message)
22 | case MESSAGE_TYPES.LOG:
23 | return this.onLogUpdate(message)
24 | case MESSAGE_TYPES.APPS_LIST_UPDATE:
25 | return this.onAppListUpdate(message)
26 | default:
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present acacode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/server/ws.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws')
2 | const _ = require('lodash')
3 | const uuid = require('short-uuid')
4 | const { MESSAGE_TYPES } = require('./constants')
5 |
6 | const sessions = {}
7 |
8 | function createWSConnection(server) {
9 | const ws = new WebSocket.Server({ server })
10 |
11 | ws.on('connection', session => {
12 | sendMessageToSession(session, MESSAGE_TYPES.CONNECTION, { status: 'ok' })
13 |
14 | const sessionId = uuid.generate()
15 | sessions[sessionId] = session
16 |
17 | session.on('close', () => {
18 | delete sessions[sessionId]
19 | session = null
20 | })
21 | })
22 | }
23 |
24 | const sendMessageToSession = (session, type, message) =>
25 | session.send(
26 | JSON.stringify({
27 | type,
28 | message,
29 | })
30 | )
31 |
32 | function sendMessage(type, message) {
33 | _.each(sessions, session => {
34 | if (session.readyState !== 3) {
35 | sendMessageToSession(session, type, message)
36 | }
37 | })
38 | }
39 |
40 | module.exports = {
41 | createWSConnection: createWSConnection,
42 | MESSAGE_TYPES: MESSAGE_TYPES,
43 | sendMessage: sendMessage,
44 | }
45 |
--------------------------------------------------------------------------------
/client/small-screens.css:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: 545px) {
2 | header .title{
3 | display: none;
4 | }
5 | .main-buttons {
6 | margin-left: auto;
7 | }
8 |
9 | .main-button:not(.toggle).run{
10 | margin-left: 30px;
11 | }
12 | .main-button.toggle.hot-keys{
13 | display: none;
14 | }
15 |
16 | .task-list-showed #task-logs{
17 | width: 100%;
18 | }
19 |
20 | #task-list{
21 | position: absolute;
22 | z-index: 1;
23 | height: calc(100% - 40px);
24 | background: inherit;
25 | transition: transform 289ms ease;
26 | }
27 | body #task-list,.task-list-showed #task-list{
28 | width: 80%;
29 | }
30 |
31 | header{
32 | height: 40px;
33 | }
34 |
35 | .tabs {
36 | transition: left 289ms ease;
37 | position: absolute;
38 | left: -32px;
39 | top: 292px;
40 | transform: rotate(90deg);
41 | z-index: 2;
42 | }
43 |
44 | .task-list-showed .tabs {
45 | left: calc(80% + 6px);
46 | }
47 | .main-button.toggle.resize {
48 | display: none;
49 | }
50 | }
--------------------------------------------------------------------------------
/server/utils/envs.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const fs = require('fs')
3 |
4 | const separateEnvsFromString = str =>
5 | _.reduce(
6 | str.split(' '),
7 | (task, substr, index, array) => {
8 | if (/^([a-zA-Z]{1,2}[a-zA-Z_0-9]{1,})=([a-zA-Z0-9]{1,})$/.test(substr)) {
9 | _.assign(task.envs, _.fromPairs([substr.split('=')]))
10 | } else {
11 | task.string.push(substr)
12 | }
13 | if (index === array.length - 1) {
14 | task.string = task.string.join(' ')
15 | }
16 | return task
17 | },
18 | {
19 | string: [],
20 | envs: {},
21 | }
22 | )
23 |
24 | const getDataFromEnvFile = filename => {
25 | let envFile = {}
26 | try {
27 | const data = fs.readFileSync(filename)
28 | envFile = _.reduce(
29 | _.compact(data.toString().split(/[\n\r]/g)),
30 | (envFile, line) => {
31 | const variable = line.split('=')
32 | envFile[variable[0]] = variable[1]
33 | return envFile
34 | },
35 | {}
36 | )
37 | } catch (e) {
38 | console.warn('.ENV file not found')
39 | }
40 | return envFile
41 | }
42 |
43 | module.exports = {
44 | separateEnvsFromString,
45 | getDataFromEnvFile,
46 | }
47 |
--------------------------------------------------------------------------------
/client/helpers/taskList.js:
--------------------------------------------------------------------------------
1 | import { createEl } from './dom_utils'
2 |
3 | export const createEmptyTaskList = tabs =>
4 | tabs.reduce((taskList, tab) => {
5 | taskList[tab.name] = []
6 | return taskList
7 | }, {})
8 |
9 | export const createTaskElement = (
10 | { isRun, isLaunching, isActive, id, name },
11 | index,
12 | { onOpenTask, onRunTask, onStopTask }
13 | ) => {
14 | return createEl('div', {
15 | className: [
16 | 'task',
17 | 'task-num-' + (index + 1),
18 | isRun && 'running',
19 | isLaunching && 'clicked',
20 | isActive && 'active',
21 | ].join(' '),
22 | id,
23 | onclick: onOpenTask,
24 | children: [
25 | createEl('i', {
26 | className: 'fas fa-cog',
27 | }),
28 | createEl('span', {
29 | className: 'task-name',
30 | innerText: name,
31 | }),
32 | createEl('button', {
33 | className: 'run-task',
34 | onclick: onRunTask,
35 | children: [
36 | createEl('i', {
37 | className: 'fas fa-play',
38 | }),
39 | ],
40 | }),
41 | createEl('button', {
42 | className: 'stop-task',
43 | onclick: onStopTask,
44 | children: [
45 | createEl('i', {
46 | className: 'fas fa-stop',
47 | }),
48 | ],
49 | }),
50 | ],
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/server/utils/commands.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const uuid = require('short-uuid')
3 | const memCache = require('./mem_cache')
4 | const { MESSAGE_TYPES, sendMessage } = require('../ws')
5 | const { separateEnvsFromString } = require('./envs')
6 |
7 | const COMMAND_DEFAULT_DATA = {
8 | logs: [],
9 | isRun: false,
10 | }
11 |
12 | const getCommandById = (configId, taskId) =>
13 | _.find(
14 | _.find(_.get(memCache.get('rc-snapshot'), 'configs', []), {
15 | id: configId,
16 | }).commands,
17 | { id: taskId }
18 | ) || {}
19 |
20 | /**
21 | * @typedef {Object} Command
22 | * @property {string} task
23 | * @property {Object} envs
24 | * @property {string} name
25 | */
26 |
27 | const createCommand = (configId, name, commandData, type) => {
28 | const commonData = separateEnvsFromString(commandData)
29 | return {
30 | ...COMMAND_DEFAULT_DATA,
31 | configId,
32 | task: commonData.string,
33 | envs: commonData.envs,
34 | name: name,
35 | id: `c${uuid.generate()}`,
36 | type,
37 | }
38 | }
39 |
40 | const updateCommand = (command, { isRun, isLaunching, isStopping, log }) => {
41 | const message = {
42 | name: command.name,
43 | isRun: command.isRun,
44 | type: command.type,
45 | id: command.id,
46 | }
47 |
48 | if (!_.isUndefined(isLaunching)) {
49 | message.isLaunching = isLaunching
50 | }
51 | if (!_.isUndefined(isStopping)) {
52 | message.isStopping = isStopping
53 | }
54 | if (!_.isUndefined(isRun)) {
55 | message.isRun = command.isRun = isRun
56 | }
57 | if (!_.isUndefined(log)) {
58 | command.logs.push(log)
59 | message.log = log
60 | }
61 | sendMessage(MESSAGE_TYPES.LOG, message)
62 | }
63 |
64 | module.exports = {
65 | COMMAND_DEFAULT_DATA,
66 | getCommandById,
67 | createCommand,
68 | updateCommand,
69 | }
70 |
--------------------------------------------------------------------------------
/server/utils/processes.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const prog = require('child_process')
3 | const emitter = require('./emitter')
4 | const kill = require('tree-kill')
5 | const isWin = require('os').platform() === 'win32'
6 |
7 | const processConfig = {
8 | file: isWin ? process.env.comspec || 'cmd.exe' : '/bin/sh',
9 | args: isWin ? ['/s', '/c'] : ['-c'],
10 | }
11 | const processes = {}
12 | const killAllListenerRefs = {}
13 |
14 | const deleteProcess = taskId => {
15 | processes[taskId].pid = null
16 | processes[taskId] = null
17 | delete processes[taskId]
18 |
19 | const emitterListeners = emitter._events.killall
20 |
21 | if (emitterListeners && emitterListeners instanceof Array) {
22 | emitterListeners.splice(killAllListenerRefs[taskId], 1)
23 | }
24 |
25 | delete killAllListenerRefs[taskId]
26 | }
27 |
28 | function killProcess(taskId) {
29 | if (processes[taskId]) {
30 | kill(processes[taskId].pid, 'SIGINT')
31 | deleteProcess(taskId)
32 | }
33 | }
34 |
35 | function attachKillListener(taskId) {
36 | emitter.once('killall', () => killProcess(taskId))
37 | killAllListenerRefs[taskId] = emitter.listeners('killall').length - 1
38 | }
39 |
40 | function getProcessById(taskId) {
41 | return processes[taskId]
42 | }
43 |
44 | /**
45 | * @param {object} command { id, rawTask, envs }
46 | * @returns spawned process
47 | */
48 | function createProcess({ id, rawTask, envs }, config) {
49 | return (processes[id] = prog.spawn(
50 | processConfig.file,
51 | [...processConfig.args, rawTask],
52 | {
53 | stdio: 'pipe',
54 | cwd: config.path.replace(/\\/g, '/'),
55 | windowsVerbatimArguments: isWin,
56 | env: _.assign(
57 | {},
58 | process.env,
59 | {
60 | PWD: config.path.replace(/\\/g, '/'),
61 | },
62 | // env variables from .env file
63 | config.envs,
64 | // env variables from command
65 | envs
66 | ),
67 | }
68 | ))
69 | }
70 |
71 | module.exports = {
72 | killProcess,
73 | attachKillListener,
74 | getProcessById,
75 | createProcess,
76 | }
77 |
--------------------------------------------------------------------------------
/client/scripts/Configs.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { el, createEl } from '../helpers/dom_utils'
3 |
4 | export default class ConfigsManager {
5 | configs = []
6 | activeConfigIndex = 0
7 |
8 | refreshConfigs = async () => {
9 | const configs = await this.getConfigs()
10 | this.configs = configs
11 | if (!this.getActiveConfig()) {
12 | this.setConfig(0)
13 | }
14 | this.refreshHtml()
15 | }
16 |
17 | refreshHtml = () => {
18 | const configsContainerEl = el(this.configsContainer)
19 | configsContainerEl.innerHTML = ''
20 |
21 | this.configs.forEach((config, index) =>
22 | createEl('div', {
23 | className: 'config',
24 | children: [
25 | createEl('span', {
26 | className: 'name',
27 | innerText: config.name,
28 | }),
29 | index &&
30 | createEl('i', {
31 | className: 'fas fa-times close-icon',
32 | onclick: () => {
33 | window.event.preventDefault()
34 | this.removeConfig(index)
35 | },
36 | }),
37 | ],
38 | onclick: () => this.setConfig(index),
39 | parent: configsContainerEl,
40 | })
41 | )
42 | }
43 |
44 | setConfig = (index = 0) => {
45 | this.activeConfigIndex = index
46 |
47 | this.onSetConfig(this.getActiveConfig())
48 | }
49 |
50 | getConfigByIndex = (index = 0) => {
51 | return this.configs[index]
52 | }
53 |
54 | getConfigById = id => _.find(this.configs, { id })
55 |
56 | removeConfig = index => {
57 | const [removedConfig] = this.configs.splice(index, 1)
58 | this.onRemoveConfig(removedConfig, index)
59 |
60 | this.refreshHtml()
61 | }
62 |
63 | getActiveConfig = () => this.configs[this.activeConfigIndex]
64 |
65 | constructor(configsContainer, { onSetConfig, getConfigs, onRemoveConfig }) {
66 | this.configsContainer = configsContainer
67 | // this.configs = configs
68 | this.onSetConfig = onSetConfig || _.noop
69 | this.onRemoveConfig = onRemoveConfig || _.noop
70 | this.getConfigs = getConfigs || _.noop
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/client/styles/hot_keys-shortcuts.css:
--------------------------------------------------------------------------------
1 |
2 | [hotkeys="true"] .task:after{
3 | position: absolute;
4 | right: 6px;
5 | bottom: 6px;
6 | font-size: 11px;
7 | text-transform: uppercase;
8 | color: #828282;
9 | text-align: center;
10 | width: 10px;
11 | }
12 | [hotkeys="true"] .task-num-1:after {
13 | content:'q';
14 | }
15 | [hotkeys="true"] .task-num-2:after {
16 | content:'w';
17 | }
18 | [hotkeys="true"] .task-num-3:after {
19 | content:'e';
20 | }
21 | [hotkeys="true"] .task-num-4:after {
22 | content:'r';
23 | }
24 | [hotkeys="true"] .task-num-5:after {
25 | content:'t';
26 | }
27 | [hotkeys="true"] .task-num-6:after {
28 | content:'y';
29 | }
30 | [hotkeys="true"] .task-num-7:after {
31 | content:'u';
32 | }
33 | [hotkeys="true"] .task-num-8:after {
34 | content:'i';
35 | }
36 | [hotkeys="true"] .task-num-9:after {
37 | content:'o';
38 | }
39 | [hotkeys="true"] .task-num-10:after {
40 | content:'p';
41 | }
42 | [hotkeys="true"] .task-num-11:after {
43 | content:'[';
44 | }
45 | [hotkeys="true"] .task-num-12:after {
46 | content:']';
47 | }
48 | [hotkeys="true"] .task-num-13:after {
49 | content:'a';
50 | }
51 | [hotkeys="true"] .task-num-14:after {
52 | content:'s';
53 | }
54 | [hotkeys="true"] .task-num-15:after {
55 | content:'d';
56 | }
57 | [hotkeys="true"] .task-num-16:after {
58 | content:'f';
59 | }
60 | [hotkeys="true"] .task-num-17:after {
61 | content:'g';
62 | }
63 | [hotkeys="true"] .task-num-18:after {
64 | content:'h';
65 | }
66 | [hotkeys="true"] .task-num-19:after {
67 | content:'j';
68 | }
69 | [hotkeys="true"] .task-num-20:after {
70 | content:'k';
71 | }
72 | [hotkeys="true"] .task-num-21:after {
73 | content:'l';
74 | }
75 | [hotkeys="true"] .task-num-22:after {
76 | content:';';
77 | }
78 | [hotkeys="true"] .task-num-23:after {
79 | content:"'";
80 | }
81 | [hotkeys="true"] .task-num-24:after {
82 | content:'z';
83 | }
84 | [hotkeys="true"] .task-num-25:after {
85 | content:'x';
86 | }
87 | [hotkeys="true"] .task-num-26:after {
88 | content:'c';
89 | }
90 | [hotkeys="true"] .task-num-27:after {
91 | content:'v';
92 | }
--------------------------------------------------------------------------------
/client/medium-screens.css:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: 923px) {
2 | .wrapper {
3 | max-width: 100%;
4 | }
5 |
6 | body {
7 | padding: 0;
8 | }
9 |
10 | .tasks-button{
11 | position: absolute;
12 | left: 0;
13 | top: 0;
14 | display: block;
15 | background: transparent;
16 | border: none;
17 | padding: 6px 10px;
18 | border-radius: 0;
19 | color: #4e4e4e;
20 | font-size: 24px;
21 | cursor:pointer;
22 | z-index: 10;
23 | }
24 | .tasks-button > .fas:before{
25 | content: "\f0c9";
26 | }
27 | .task-list-showed .tasks-button > .fas:before{
28 | content: "\f00d";
29 | }
30 | header .title {
31 | padding-left: 50px;
32 | }
33 | #task-list {
34 | transform: translateX(-100%);
35 | width: 0;
36 | }
37 |
38 | #task-logs {
39 | width: 100%;
40 | }
41 | .task-list-showed #task-list {
42 | transform: translateX(0);
43 | }
44 | .task.new-task, .task-list-showed #task-list{
45 | width: 30%;
46 | }
47 | .active-tab-procfile .task.new-task,.active-tab-procfile .task.new-task.show{
48 | transform: scaleY(.6) translateY(58px);
49 | }
50 | .task-list-showed .active-tab-procfile .task.new-task{
51 | transform: none
52 | }
53 | .task-list-showed #task-logs {
54 | width: 70%;
55 | }
56 | .task-list-showed .tabs {
57 | position: absolute;
58 | left: calc(30% + 12px);
59 | top: 37px;
60 | transform: rotate(0deg);
61 | z-index: 2;
62 | }
63 | .task-list-showed .tab {
64 | border-radius: 0 0 8px 8px;
65 | }
66 | .logs-container {
67 | padding-top: 27px;
68 | }
69 | .scrolling > .task-data {
70 | top: 45px;
71 | }
72 |
73 | .main-button.toggle{
74 | min-width: 40px;
75 | }
76 | .main-button.toggle span{
77 | display: none;
78 | }
79 |
80 | .task-list-showed .active-tab-procfile .task.new-task.show {
81 | transform: translateY(-128px);
82 | }
83 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const TerserJSPlugin = require('terser-webpack-plugin')
3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
4 |
5 | const srcFolder = process.env.SRC_DIR || path.resolve(__dirname, './client')
6 | const destFolder = process.env.DEST_DIR || path.resolve(__dirname, './dist')
7 |
8 | const isDev = process.env.NODE_ENV === 'development'
9 |
10 | module.exports = {
11 | devtool: isDev ? 'eval' : false,
12 | mode: isDev ? 'development' : 'production',
13 | target: 'web',
14 | entry: {
15 | 'index.html': path.resolve(srcFolder, './index.html'),
16 | global: path.resolve(srcFolder, './global.js'),
17 | },
18 | output: {
19 | path: destFolder,
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.html$/,
25 | loaders: [
26 | 'file-loader?name=[name].html',
27 | 'extract-loader',
28 | {
29 | loader: 'html-loader',
30 | options: {
31 | minimize: !isDev,
32 | root: srcFolder,
33 | attrs: ['img:src', 'link:href'],
34 | },
35 | },
36 | ],
37 | },
38 | {
39 | test: /\.(png|jpg|gif)$/,
40 | loaders: [
41 | {
42 | loader: 'url-loader',
43 | options: {
44 | limit: 8192,
45 | fallback: 'responsive-loader',
46 | quality: 75,
47 | },
48 | },
49 | ],
50 | },
51 | {
52 | test: /\.js$/,
53 | loaders: [
54 | {
55 | loader: 'babel-loader',
56 | options: {
57 | babelrc: true,
58 | },
59 | },
60 | ],
61 | },
62 | {
63 | test: /\.css$/,
64 | use: ['file-loader', 'extract-loader', 'css-loader'],
65 | },
66 | {
67 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
68 | use: [
69 | {
70 | loader: 'file-loader',
71 | options: {
72 | name: '[name].[ext]',
73 | outputPath: 'fonts/',
74 | },
75 | },
76 | ],
77 | },
78 | ],
79 | },
80 | optimization: {
81 | minimizer: isDev
82 | ? []
83 | : [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
84 | },
85 | // plugins: [
86 | // new HtmlWebpackPlugin({
87 | // minify: {
88 | // html5: true,
89 | // removeComments: true,
90 | // collapseWhitespace: true,
91 | // minifyCSS: true,
92 | // minifyJS: true,
93 | // // minifyURLs: true
94 | // },
95 | // template: entryHtmlFileAbsolutePath,
96 | // }),
97 | // ],
98 | }
99 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 |
5 | cache:
6 | directories:
7 | - "node_modules"
8 |
9 | before_install:
10 | - export PACKAGE_VERSION=$(node -p "require('./package.json').version")
11 | - export NPM_REMOTE_VERSION=$(npm view flamebird version)
12 | - export PACKAGE_NAME=$(node -p "require('./package.json').name")
13 | - export BRANCH_IS_TAG=$(node -p "/^([0-9].[0-9].[0-9]+((-(alpha|beta))|))$/.test('${TRAVIS_BRANCH}')")
14 | - export PROJECT_NAME="acacode/flamebird"
15 | - export RELEASE_BODY=$(node -p "'[Click here to find current release changes](https://github.com/$PROJECT_NAME/blob/$TRAVIS_BRANCH/CHANGELOG.md)'")
16 |
17 | install:
18 | - npm ci
19 |
20 | jobs:
21 | include:
22 | - stage: pre-test(lint,audit)
23 | script:
24 | - npm audit
25 | - npm run lint
26 | - stage: test
27 | script:
28 | - npm run build
29 | - npm run test
30 | - stage: publish
31 | if: env(BRANCH_IS_TAG) != true AND branch = master AND type = push
32 | name: "Create Github Release"
33 | before_deploy:
34 | - git config --global user.email "builds@travis-ci.com"
35 | - git config --global user.name "Travis CI"
36 | - git tag $PACKAGE_VERSION -a -m "Generated tag from TravisCI for build $TRAVIS_BUILD_NUMBER from $TRAVIS_BRANCH"
37 | - git push -q https://$GITHUB_TOKEN@github.com/$PROJECT_NAME --tags
38 | - zip -r release.zip dist flamebird.js LICENSE postinstall.js README.md client server package-lock.json package.json
39 | - ls -l
40 | deploy:
41 | provider: releases
42 | name: "Release ${PACKAGE_VERSION}"
43 | body: "$RELEASE_BODY"
44 | overwrite: true
45 | skip_cleanup: true
46 | api_key:
47 | secure: ${GITHUB_TOKEN}
48 | file:
49 | - release.zip
50 | on:
51 | all_branches: true
52 | repo: $PROJECT_NAME
53 | after_deploy:
54 | - echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
55 | - npm publish
56 |
57 | notifications:
58 | slack:
59 | rooms:
60 | secure: oEsx4yOk0AkiYMrjhDbKzh7mJIHd/Rn1Y+C86bmxGxCbAc/VuAM1pRfk/A3o7I0QwDzFZ1XCZrHT1VsYDj4ypFaJb8pCtwOU0pv+mseGlcYOvtV5NgQMBM3HQvx9+9iF8sLXg1glzToNkd8iBkbNxEgg6EEeBdX3mD7QX04xl4Mgl259gofN1pOxP3LwHKkKcZgeS6MAEfWhBG/a8poAccvusgn75wNZ1qyI+ZPYtQ75JK4QieJzmxTHPr2gGhq1EwRWYIjjyIEA4ofQpjWyzy0U6OQKBxwPaH+GM+Vn6hhp5JOenmiIslUZmzgyXL233Iu4iMvh1AiLJvB7AIBDilA3CYvCzM5uQVyKqiNFMrkcBRZgE3FCAaYYcpfr+k+3wO3HGstvcW+xxB3EdOGfXuI6Bu31k6kQQjXTZnYCGXgH4b+s+CkknOt04VXMxwPaQNjwb7+2HZt7En0kCAcswCB2ZYekfwJ8ps+ahlKGDbPEgg/vYAPV0JqwY4gEGFOthe3MhHDzpnjm6udpItlYrlcqzV4w4b3LEOgc5AomrQRD+SPdFHwvUjtjyDjyhdPNK9QixCIPW55njCssCJOi5kwq74zr3eVvqD8ijmUdfL6ASu1113fs/4k58mX4GwEkmMicThLg01aoppd3IWTc//bsYOLC+AbC8cAm9JZZWHA=
61 |
--------------------------------------------------------------------------------
/server/processWorker.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const {
3 | killProcess,
4 | attachKillListener,
5 | getProcessById,
6 | createProcess,
7 | } = require('./utils/processes')
8 | const { updateCommand } = require('./utils/commands')
9 |
10 | const colors = require('colors')
11 | const kill = require('tree-kill')
12 | const { getConfig } = require('./config')
13 |
14 | colors.enabled = true
15 |
16 | function stop(command) {
17 | // if (_.isString(command)) {
18 | // command = getCommandById(command)
19 | // }
20 |
21 | killProcess(command.id)
22 | updateCommand(command, { isStopping: true })
23 | }
24 |
25 | function run(command) {
26 | // if (_.isString(command)) {
27 | // command = getCommandById(command)
28 | // }
29 |
30 | const config = getConfig(command.configId)
31 | const taskId = command.id
32 | const isWeb = getConfig(command.configId).web
33 | const proc = createProcess(command, config)
34 |
35 | if (isWeb) {
36 | updateCommand(command, { isRun: true })
37 |
38 | proc.stdout.on('data', rawLog =>
39 | updateCommand(command, {
40 | log: rawLog.toString(),
41 | isRun: true,
42 | })
43 | )
44 | proc.stderr.on('data', rawLog =>
45 | updateCommand(command, {
46 | log: rawLog.toString(),
47 | isRun: true,
48 | })
49 | )
50 | proc.on('close', code => {
51 | killProcess(taskId)
52 | updateCommand(command, {
53 | isRun: false,
54 | log: !code ? 'Exited Successfully' : 'Exited with exit code ' + code,
55 | })
56 | })
57 | proc.on('error', () => {
58 | killProcess(taskId)
59 | updateCommand(command, {
60 | isRun: false,
61 | log: 'Failed to execute command',
62 | })
63 | })
64 |
65 | attachKillListener(taskId)
66 | } else {
67 | const defaultOutputHandler = log => {
68 | process.stdout.write(log.toString())
69 | }
70 | proc.stdout.on('data', defaultOutputHandler)
71 | proc.stdout.on('error', defaultOutputHandler)
72 | proc.stderr.on('data', defaultOutputHandler)
73 | }
74 | }
75 |
76 | function reRun(command) {
77 | const isLaunching = true
78 | const proc = getProcessById(command.id)
79 |
80 | if (proc) {
81 | kill(proc.pid, 'SIGINT', () => {
82 | updateCommand(command, { isLaunching })
83 | setTimeout(() => run(command), 1000)
84 | })
85 | updateCommand(command, { isLaunching })
86 | } else {
87 | updateCommand(command, { isLaunching })
88 | run(command)
89 | }
90 | }
91 |
92 | function runAll(commands) {
93 | _.each(commands, run)
94 | }
95 | function stopAll(commands) {
96 | _.each(commands, stop)
97 | }
98 |
99 | module.exports.runAll = runAll
100 | module.exports.stopAll = stopAll
101 | module.exports.run = run
102 | module.exports.stop = stop
103 | module.exports.reRun = reRun
104 |
--------------------------------------------------------------------------------
/client/styles/dark-theme.css:
--------------------------------------------------------------------------------
1 |
2 | body[theme="dark"] {
3 | background: #212121;
4 | }
5 |
6 | [theme="dark"] header {
7 | background: #3e3e3e;
8 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.33);
9 | }
10 |
11 | [theme="dark"] .title {
12 | color: #eeeeee;
13 | background: #3e3e3e;
14 | }
15 |
16 | [theme="dark"] .wrapper {
17 | background: #424242;
18 | }
19 |
20 |
21 | [theme="dark"] .task.active {
22 | background: rgb(37, 37, 37);
23 | color: #ebebeb;
24 | }
25 |
26 |
27 | [theme="dark"] .task {
28 | color: #a5a5a5;
29 | }
30 |
31 |
32 | [theme="dark"] .task:hover {
33 | background: rgb(37, 37, 37);
34 | color: #ebebeb;
35 | }
36 |
37 |
38 |
39 | [theme="dark"] button.tab {
40 | color: #d1d1d1;
41 | background: rgb(69, 69, 69);
42 | box-shadow: 0 0px 0px 0.5px rgb(33, 33, 33);
43 | }
44 |
45 |
46 | [theme="dark"] button.tab.active,
47 | [theme="dark"] button.tab:active {
48 | background: rgb(159, 144, 55);
49 | color: #212121;
50 | }
51 |
52 | [theme="dark"] .logs-container{
53 | color: #dcdcdc;
54 | }
55 |
56 | [theme="dark"] .ansi-red-fg {
57 | color: rgb(230, 78, 78);
58 | }
59 |
60 | [theme="dark"] .logs-container::-webkit-scrollbar-thumb {
61 | background: #c1c1c1;
62 | }
63 |
64 | [theme="dark"] .logs-container::-webkit-scrollbar-thumb:hover {
65 | background: #9e9e9e;
66 | }
67 |
68 | [theme="dark"] .logs-container{
69 | background: #060606;
70 | }
71 |
72 | [theme="dark"] .ansi-bright-black-fg {
73 | color: #909090;
74 | }
75 |
76 | [theme="dark"] .main-button.toggle {
77 | background: #333333;
78 | color: #929292;
79 | }
80 |
81 | [theme="dark"] .main-button.toggle.active {
82 | background: #2e7b58;
83 | color: #b5e0cd;
84 | }
85 |
86 |
87 | [theme="dark"] .run{
88 | background: #2f9a6a;
89 | }
90 |
91 | [theme="dark"] .stop{background: #8c4343;}
92 |
93 |
94 | [theme="dark"] .task.new-task .form input,[theme="dark"] .task.new-task .form textarea {
95 | background: #333;
96 | color: #dcdcdc;
97 | }
98 |
99 | [theme="dark"] .task.new-task .form button {
100 | background: #2e7b58;
101 | color: #b5e0cd;
102 | }
103 |
104 |
105 | [theme="dark"] .task.updated{
106 | animation-name: dark-highlight;
107 | }
108 |
109 | [theme="dark"] .toggle.color .fas {
110 | transform: scaleX(-1);
111 | }
112 |
113 | [theme="dark"] .close-icon {
114 | color: #995959;
115 | }
116 |
117 | [theme="dark"] .configs-list {
118 | background: #424242;
119 | }
120 |
121 | [theme="dark"] .config {
122 | color: #aeaeae;
123 | }
124 |
125 | [theme="dark"] .config:hover {
126 | background: #2b2b2b;
127 | }
128 |
129 | [theme="dark"] .config:not(:first-child) {
130 | border-color: rgba(255, 255, 255, 0.05);
131 | }
132 |
133 |
134 | @keyframes dark-highlight{
135 | 50%{
136 | background: rgb(79, 80, 63);
137 | }
138 | }
--------------------------------------------------------------------------------
/client/scripts/Tabs.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { el, createEl } from '../helpers/dom_utils'
3 | import { createTab } from '../helpers/tabs'
4 |
5 | const WRAPPER_ELEMENT = '.wrapper'
6 | const TABS_CONTAINER = '#tabs'
7 | const ACTIVE_TAB_CLASSNAME = 'active'
8 |
9 | export const PRIORITY_TAB = createTab('procfile', true)
10 |
11 | export const DEFAULT_TABS = [
12 | createTab('npm'),
13 | PRIORITY_TAB,
14 | createTab('grunt'),
15 | createTab('gulp'),
16 | ]
17 |
18 | class Tabs {
19 | wrapper = null
20 | onChangeListeners = []
21 |
22 | tabs = [...DEFAULT_TABS]
23 |
24 | activeTab = _.find(this.tabs, { active: true })
25 |
26 | constructor(wrapperElementQuery, tabsContainerQuery) {
27 | this.tabsContainer = el(tabsContainerQuery)
28 | this.wrapper = el(wrapperElementQuery)
29 | }
30 |
31 | createTabs = tabs => {
32 | this.tabsContainer.innerHTML = ''
33 |
34 | this.tabs = tabs
35 | this.tabs.forEach(tab => {
36 | createEl('button', {
37 | className: 'tab' + (tab.active ? ` ${ACTIVE_TAB_CLASSNAME}` : ''),
38 | id: tab.name,
39 | innerText: tab.name,
40 | onclick: () => this.setActive(tab.name),
41 | parent: this.tabsContainer,
42 | })
43 | })
44 | }
45 |
46 | getAll = () => this.tabs
47 |
48 | getTabBy = partialTabData => _.find(this.tabs, partialTabData)
49 |
50 | getNotActiveTabs = () => this.getTabBy({ active: false })
51 |
52 | getTab = name => this.getTabBy({ name })
53 |
54 | getActive = () => this.activeTab
55 |
56 | setActive = name => {
57 | const prevTab = this.activeTab
58 | // if (prevTab.name !== name) {
59 | if (this.getTab(prevTab.name)) {
60 | prevTab.active = false
61 | el(`#${prevTab.name}`).classList.remove(ACTIVE_TAB_CLASSNAME)
62 | }
63 | this.activeTab = this.getTab(name)
64 | this.activeTab.active = true
65 |
66 | el(`#${this.activeTab.name}`).classList.add(ACTIVE_TAB_CLASSNAME)
67 |
68 | if (this.onChangeListeners.length) {
69 | this.onChangeListeners.forEach(listener =>
70 | listener(this.activeTab.name, prevTab.name)
71 | )
72 | }
73 | this.wrapper.className = `wrapper active-tab-${this.activeTab.name}`
74 | return this.activeTab
75 | }
76 |
77 | setNextAsActive() {
78 | const index = this.tabs.findIndex(tab => tab.name === this.activeTab.name)
79 | if (_.isUndefined(this.tabs[index + 1])) {
80 | this.setActive(this.tabs[0].name)
81 | } else {
82 | this.setActive(this.tabs[index + 1].name)
83 | }
84 | }
85 |
86 | listenChanges = callback => {
87 | this.onChangeListeners.push(callback)
88 | }
89 |
90 | removeTab = tab => {
91 | const removingTabIndex = this.tabs.findIndex(
92 | ({ name }) => tab.name === name
93 | )
94 | if (removingTabIndex !== -1) {
95 | this.tabs.splice(removingTabIndex, 1)
96 | el(`#${tab.name}`).remove()
97 | }
98 | }
99 | }
100 |
101 | export default new Tabs(WRAPPER_ELEMENT, TABS_CONTAINER)
102 |
--------------------------------------------------------------------------------
/client/controllers/Header.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import WindowAttached from '../helpers/WindowAttached'
3 | import ThemeSwitcher, { THEMES } from '../scripts/ThemeSwitcher'
4 | import { toggleClass, el as getEl } from '../helpers/dom_utils'
5 |
6 | export class Header extends WindowAttached('header') {
7 | themeSwitcher = new ThemeSwitcher({
8 | onSwitchTheme: newTheme => {
9 | $('.toggle.color').toggleClass('active', newTheme === THEMES.DARK)
10 | },
11 | })
12 |
13 | deps = {
14 | hotkeys: null,
15 | }
16 |
17 | configShowed = false
18 | fullscreen = !!localStorage.getItem('fullscreen')
19 | hotKeysEnabled = !!localStorage.getItem('hotkeys')
20 | notificationsEnabled = !!localStorage.getItem('notifications')
21 |
22 | constructor({ hotkeys }) {
23 | super()
24 |
25 | this.deps.hotkeys = hotkeys
26 |
27 | $(document).ready(() => {
28 | this.themeSwitcher.setTheme(localStorage.getItem('theme') || 'white')
29 |
30 | this.updateFullscreen()
31 | this.updateHotkeys()
32 | this.updateNotifications()
33 |
34 | window.addEventListener('click', () => {
35 | if (this.configShowed) {
36 | this.handleTitleClick(false)
37 | }
38 | })
39 | })
40 | }
41 |
42 | handleThemeIconClick = () => {
43 | this.themeSwitcher.switchTheme()
44 | }
45 |
46 | handleResizeIconClick = () => {
47 | this.fullscreen = !this.fullscreen
48 | this.updateFullscreen()
49 | }
50 |
51 | handleNotificationsIconClick = () => {
52 | this.notificationsEnabled = !this.notificationsEnabled
53 | this.updateNotifications()
54 | }
55 |
56 | handleTitleClick = overrideFlag => {
57 | if (typeof overrideFlag === 'boolean') {
58 | this.configShowed = overrideFlag
59 | } else {
60 | this.configShowed = !this.configShowed
61 | window.event.stopPropagation()
62 | }
63 | document.body.setAttribute('config', this.configShowed ? 'show' : 'hide')
64 | }
65 |
66 | handleKeyboardIconClick = () => {
67 | this.hotKeysEnabled = !this.hotKeysEnabled
68 | this.updateHotkeys()
69 | }
70 |
71 | updateNotifications() {
72 | toggleClass(
73 | getEl('.main-button.notifications'),
74 | 'active',
75 | this.notificationsEnabled
76 | )
77 | if (this.notificationsEnabled) {
78 | localStorage.setItem('notifications', true)
79 | } else {
80 | delete localStorage.notifications
81 | $('.task.updated').removeClass('updated')
82 | }
83 | }
84 |
85 | updateFullscreen() {
86 | toggleClass(getEl('.main-button.resize'), 'active', this.fullscreen)
87 |
88 | if (this.fullscreen) {
89 | localStorage.setItem('fullscreen', true)
90 | document.body.setAttribute('fullscreen', 'true')
91 | } else {
92 | document.body.removeAttribute('fullscreen')
93 | delete localStorage.fullscreen
94 | }
95 | }
96 |
97 | updateHotkeys() {
98 | toggleClass(getEl('.main-button.hot-keys'), 'active', this.hotKeysEnabled)
99 | this.deps.hotkeys.triggerEnabled(this.hotKeysEnabled)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flamebird
7 |
8 |
9 |
10 |
11 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | const program = require('commander')
2 | const fs = require('fs')
3 | const _ = require('lodash')
4 | const path = require('path')
5 | const uuid = require('short-uuid')
6 |
7 | const memCache = require('./utils/mem_cache')
8 | const { getDataFromEnvFile } = require('./utils/envs')
9 | const taskfile = require('./taskfile')
10 |
11 | const RC_FILE_NAME = '.flamebirdrc'
12 | const RC_FILE_PATH = path.resolve(__dirname, `../${RC_FILE_NAME}`)
13 | const DEFAULT_CONFIG = {
14 | configs: [],
15 | }
16 |
17 | const getRC = () => {
18 | let config = {}
19 | try {
20 | config = JSON.parse(fs.readFileSync(RC_FILE_PATH))
21 | if (!config) {
22 | throw new Error("Wrong flamebird rc file. Let's create new")
23 | }
24 | } catch (e) {
25 | config = DEFAULT_CONFIG
26 | updateRC(config)
27 | }
28 | return config
29 | }
30 |
31 | const updateRC = rc => {
32 | // console.log('updateRC', rc)
33 | fs.writeFileSync(RC_FILE_PATH, JSON.stringify(rc))
34 | }
35 |
36 | const refreshRC = () => {
37 | console.log('refreshRC')
38 | const rc = getRC()
39 | memCache.set('rc-snapshot', rc)
40 | return rc
41 | }
42 |
43 | const createConfig = (
44 | {
45 | ignoreTrs,
46 | name,
47 | port,
48 | tasks,
49 | taskRunner,
50 | package: useOnlyPackageJson,
51 | withoutBrowser,
52 | sortByName,
53 | },
54 | isWeb
55 | ) => {
56 | const rc = getRC()
57 |
58 | console.log('createConfig')
59 |
60 | const config = {
61 | main: !rc.configs || !rc.configs.length,
62 | id: memCache.set('id', uuid.generate()),
63 | pid: process.pid,
64 | path: path.resolve(),
65 | withoutTaskRunner: !!ignoreTrs,
66 | name: name,
67 | port: +port,
68 | tasks: _.compact(_.split(tasks, /,/g)),
69 | taskRunner: taskRunner,
70 | useOnlyPackageJson: !!useOnlyPackageJson,
71 | web: !!isWeb,
72 | withoutBrowser: !!withoutBrowser,
73 | sortByName: !!sortByName,
74 | envs: getDataFromEnvFile(program.env),
75 | }
76 |
77 | if (config.web) {
78 | const rcSnapshot = memCache.set('rc-snapshot', {
79 | configs: [
80 | ...rc.configs,
81 | _.merge(config, {
82 | commands: taskfile.load(config, program.procfile),
83 | }),
84 | ],
85 | })
86 | updateRC({
87 | configs: _.map(rcSnapshot.configs, config => ({
88 | ...config,
89 | commands: _.map(config.commands, command =>
90 | _.merge(command, { logs: [] })
91 | ),
92 | })),
93 | })
94 | }
95 |
96 | return config
97 | }
98 |
99 | const findConfig = findHandler =>
100 | _.find(_.get(memCache.get('rc-snapshot'), 'configs', []), findHandler) || null
101 |
102 | const getConfig = id => (id ? findConfig({ id }) : getMainConfig())
103 |
104 | const getMainConfig = () => findConfig({ main: true })
105 |
106 | const isMainConfig = () => memCache.get('config').main
107 |
108 | const removeConfig = configId => {
109 | const rcSnapshot = memCache.get('rc-snapshot')
110 | const configIndex = rcSnapshot.configs.findIndex(
111 | config => configId === config.id
112 | )
113 | if (configIndex > -1) rcSnapshot.configs.splice(configIndex, 1)
114 |
115 | memCache.set('rc-snapshot', rcSnapshot)
116 | updateRC(rcSnapshot)
117 | }
118 |
119 | module.exports = {
120 | getRC,
121 | refreshRC,
122 | getMainConfig,
123 | getConfig,
124 | isMainConfig,
125 | createConfig,
126 | removeConfig,
127 | updateRC,
128 | }
129 |
--------------------------------------------------------------------------------
/client/helpers/dom_utils.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import _ from 'lodash'
3 |
4 | export const createButton = function(
5 | classes,
6 | icon,
7 | onClick,
8 | asJqElement,
9 | innerText
10 | ) {
11 | let button
12 | if (asJqElement) {
13 | button = $(' ')
14 | button.addClass(classes)
15 | if (icon) {
16 | const iconEl = $(' ')
17 | button.html(iconEl)
18 | }
19 | if (innerText) {
20 | button.append(window.createSpan('', innerText))
21 | }
22 | if (onClick) {
23 | button.on('click', onClick)
24 | }
25 | } else {
26 | button =
27 | '' +
32 | (icon ? ' ' : '') +
33 | (innerText || '') +
34 | ' '
35 | }
36 |
37 | return button
38 | }
39 |
40 | export const createSpan = function(classes, text) {
41 | return '' + text + ' '
42 | }
43 |
44 | export const funcToStr = function(func) {
45 | const args = _.slice(arguments, 1)
46 | return (
47 | func.name +
48 | '(' +
49 | _(args)
50 | .map(function(arg) {
51 | switch (typeof arg) {
52 | case 'string':
53 | return "'" + arg + "'"
54 | default:
55 | return arg
56 | }
57 | })
58 | .join(',') +
59 | ')'
60 | )
61 | }
62 |
63 | export const createDiv = function(
64 | classes,
65 | innerText,
66 | icon,
67 | id,
68 | onClick,
69 | asJqElement
70 | ) {
71 | let element =
72 | '' +
78 | (icon ? ' ' : '') +
79 | (innerText || '') +
80 | '
'
81 | if (asJqElement) {
82 | element = $(element)
83 | if (onClick) {
84 | element.on('click', onClick)
85 | }
86 | }
87 | return element
88 | }
89 |
90 | export const createEnvsInput = function(key, value) {
91 | return (
92 | ' '
99 | )
100 | }
101 |
102 | export const toggleClass = (element, className, isShow) =>
103 | element && element.classList[isShow ? 'add' : 'remove'](className)
104 |
105 | export const addClass = (element, className) =>
106 | element && element.classList.add(className)
107 |
108 | export const removeClass = (element, className) =>
109 | element && element.classList.remove(className)
110 |
111 | export const el = (query, asList) =>
112 | document[`querySelector${asList ? 'All' : ''}`](query)
113 |
114 | export const createEl = (tag, options) => {
115 | const element = document.createElement(tag)
116 | _.each(options, (option, name) => {
117 | if (name !== 'parent' && name !== 'children') element[name] = option
118 | })
119 | if (options.parent) {
120 | const parentElement = _.isString(options.parent)
121 | ? el(options.parent)
122 | : options.parent
123 | parentElement.appendChild(element)
124 | }
125 | if (options.children && options.children) {
126 | _.each(options.children, child => {
127 | if (child)
128 | element.appendChild(
129 | typeof child === 'string' ? document.createTextNode(child) : child
130 | )
131 | })
132 | }
133 | return element
134 | }
135 |
--------------------------------------------------------------------------------
/client/scripts/HotKeys.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import $ from 'jquery'
3 |
4 | const TASK_CHAR_CODES = [
5 | 81,
6 | 87,
7 | 69,
8 | 82,
9 | 84,
10 | 89,
11 | 85,
12 | 73,
13 | 79,
14 | 80,
15 | 219,
16 | 221,
17 | 65,
18 | 83,
19 | 68,
20 | 70,
21 | 71,
22 | 72,
23 | 74,
24 | 75,
25 | 76,
26 | 186,
27 | 222,
28 | 90,
29 | 88,
30 | 67,
31 | 86,
32 | 66,
33 | 78,
34 | 77,
35 | 188,
36 | 190,
37 | 191,
38 | ]
39 |
40 | // const TASK_LIST_ELEMENTS_QUERY = '#task-list .task'
41 |
42 | const getTaskQueryByKeyCode = keyCode => $(`.task[char-code="${keyCode}"]`)
43 |
44 | export default class HotKeys {
45 | isEnabled = false
46 | taskCharCodes = TASK_CHAR_CODES
47 |
48 | actions = {
49 | tab: _.noop,
50 | arrowUp: _.noop,
51 | arrowDown: _.noop,
52 | del: _.noop,
53 | shiftA: _.noop,
54 | shiftS: _.noop,
55 | shiftR: _.noop,
56 | shiftArrowUp: _.noop,
57 | shiftArrowDown: _.noop,
58 | }
59 |
60 | keyCodeActions = {
61 | // [shift] key is not triggered
62 | 0: {
63 | 38: 'arrowUp',
64 | 40: 'arrowDown',
65 | 46: 'del',
66 | 9: 'tab',
67 | },
68 | // [shift] key is triggered
69 | 1: {
70 | 38: 'shiftArrowUp',
71 | 40: 'shiftArrowDown',
72 | 65: 'shiftA',
73 | 82: 'shiftR',
74 | 83: 'shiftS',
75 | },
76 | }
77 |
78 | clearifyEvent = event => {
79 | // event.keyCode = 0
80 | // event.ctrlKey = false
81 | // event.cancelBubble = true
82 | event.preventDefault()
83 | event.stopPropagation()
84 | }
85 |
86 | onKeyClick = event => {
87 | const {
88 | shiftKey,
89 | keyCode,
90 | target: { tagName },
91 | } = event
92 | if (_.indexOf(['INPUT', 'TEXTAREA'], tagName) === -1) {
93 | if (shiftKey) {
94 | this.clearifyEvent(event)
95 | }
96 | const actionName = this.keyCodeActions[+shiftKey][keyCode]
97 | if (actionName) {
98 | this.actions[actionName](event)
99 | } else if (!shiftKey) {
100 | getTaskQueryByKeyCode(keyCode).trigger('click')
101 | }
102 | }
103 | }
104 |
105 | triggerEnabled(isEnabled) {
106 | this.isEnabled = !!isEnabled
107 | if (this.isEnabled) {
108 | this.connect()
109 | } else {
110 | this.disconnect()
111 | }
112 | }
113 |
114 | connect() {
115 | window.addEventListener('keydown', this.onKeyClick, false)
116 | localStorage.setItem('hotkeys', true)
117 | document.body.setAttribute('hotkeys', 'true')
118 |
119 | // setTimeout(() => {
120 | // _.each(
121 | // document.querySelectorAll(TASK_LIST_ELEMENTS_QUERY),
122 | // (element, keycodeIndex) => {
123 | // element.setAttribute('char-code', this.taskCharCodes[keycodeIndex])
124 | // }
125 | // )
126 | // })
127 | }
128 |
129 | disconnect() {
130 | window.removeEventListener('keydown', this.onKeyClick, false)
131 | delete localStorage.hotkeys
132 | document.body.removeAttribute('hotkeys')
133 | // setTimeout(() => {
134 | // _.each(document.querySelectorAll(TASK_LIST_ELEMENTS_QUERY), element => {
135 | // element.removeAttribute('char-code')
136 | // })
137 | // })
138 | }
139 |
140 | connectTaskButton = (taskEl, index) => {
141 | taskEl.setAttribute('char-code', this.taskCharCodes[index])
142 | }
143 |
144 | constructor({
145 | tab,
146 | arrowUp,
147 | arrowDown,
148 | del,
149 | shiftA,
150 | shiftS,
151 | shiftR,
152 | shiftArrowUp,
153 | shiftArrowDown,
154 | } = {}) {
155 | // this.triggerEnabled(true)
156 |
157 | _.each(
158 | {
159 | tab,
160 | arrowUp,
161 | arrowDown,
162 | del,
163 | shiftA,
164 | shiftS,
165 | shiftR,
166 | shiftArrowUp,
167 | shiftArrowDown,
168 | },
169 | (handler, handlerName) => {
170 | this.actions[handlerName] = handler
171 | }
172 | )
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flamebird",
3 | "version": "3.0.0-alpha",
4 | "description": "wonderful task manager for Procfile-based and npm-based applications",
5 | "main": "flamebird.js",
6 | "scripts": {
7 | "test": "echo \"No tests\" && exit 0",
8 | "clean": "rimraf dist",
9 | "lint-js": "eslint .",
10 | "lint": "npm run lint-js",
11 | "lint:fix": "npm run lint -- --fix",
12 | "build": "npm run clean && npm run client",
13 | "postinstall": "node postinstall.js",
14 | "server": "node flamebird.js web -p 7733 -w",
15 | "server:dev": "cross-env NODE_ENV=development npm run server",
16 | "client": "webpack --config webpack.config.js -p",
17 | "client:dev": "cross-env NODE_ENV=development npm run client",
18 | "debug": "nodemon --exec \"npm run client:dev\" --watch client -V"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/acacode/flamebird.git"
23 | },
24 | "keywords": [
25 | "task",
26 | "tasks",
27 | "procfile",
28 | "taskmanager",
29 | "taskrunner",
30 | "foreman",
31 | "node-foreman",
32 | "npm-package",
33 | "js",
34 | "task-manager",
35 | "task-sheduler",
36 | "job-sheduler",
37 | "nodejs-utility",
38 | "task-utility",
39 | "node-process",
40 | "process-manager",
41 | "manager",
42 | "process-runner",
43 | "process-run"
44 | ],
45 | "author": "acacode",
46 | "bin": {
47 | "flamebird": "flamebird.js",
48 | "fb": "flamebird.js"
49 | },
50 | "license": "MIT",
51 | "dependencies": {
52 | "@babel/runtime": "^7.5.5",
53 | "ansi_up": "^4.0.4",
54 | "body-parser": "^1.19.0",
55 | "colors": "^1.3.3",
56 | "commander": "^2.20.0",
57 | "express": "^4.17.1",
58 | "grunt": "^1.0.4",
59 | "grunt-available-tasks": "^0.6.3",
60 | "gulp-task-listing": "^1.1.0",
61 | "jquery": "^3.4.1",
62 | "kinka": "^2.5.6",
63 | "lodash": "^4.17.15",
64 | "moment": "^2.24.0",
65 | "opn": "^6.0.0",
66 | "redom": "^3.23.1",
67 | "short-uuid": "^3.1.1",
68 | "tree-kill": "^1.2.1",
69 | "ws": "^7.0.1"
70 | },
71 | "devDependencies": {
72 | "@babel/core": "^7.4.5",
73 | "@babel/plugin-proposal-class-properties": "^7.5.5",
74 | "@babel/plugin-proposal-export-default-from": "^7.5.2",
75 | "@babel/plugin-proposal-export-namespace-from": "^7.5.2",
76 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
77 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
78 | "@babel/plugin-transform-runtime": "^7.5.5",
79 | "@babel/preset-env": "^7.5.5",
80 | "@babel/preset-stage-0": "^7.0.0",
81 | "ajv": "^6.10.0",
82 | "babel-eslint": "^10.0.2",
83 | "babel-loader": "^8.0.6",
84 | "babel-plugin-import": "^1.12.0",
85 | "babel-plugin-transform-class-properties": "^6.24.1",
86 | "babel-plugin-transform-export-extensions": "^6.22.0",
87 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
88 | "babel-polyfill": "^6.26.0",
89 | "babel-runtime": "^6.26.0",
90 | "copy-webpack-plugin": "^5.0.3",
91 | "cross-env": "^5.2.0",
92 | "css-loader": "1.0.0",
93 | "dree": "^2.1.10",
94 | "eslint": "^6.2.2",
95 | "eslint-config-prettier": "^6.1.0",
96 | "eslint-config-standard": "^14.0.1",
97 | "eslint-plugin-import": "^2.18.2",
98 | "eslint-plugin-node": "^9.1.0",
99 | "eslint-plugin-prettier": "^3.1.0",
100 | "eslint-plugin-promise": "^4.2.1",
101 | "eslint-plugin-standard": "^4.0.1",
102 | "extract-loader": "^3.1.0",
103 | "file-loader": "^4.2.0",
104 | "handlebars-loader": "^1.7.1",
105 | "html-loader": "^0.5.5",
106 | "html-minifier-webpack-plugin": "^2.2.1",
107 | "html-webpack-plugin": "^3.2.0",
108 | "jimp": "^0.6.4",
109 | "mini-css-extract-plugin": "^0.7.0",
110 | "nodemon": "^1.19.1",
111 | "optimize-css-assets-webpack-plugin": "^5.0.3",
112 | "prettier": "^1.18.2",
113 | "raw-loader": "^3.1.0",
114 | "responsive-loader": "^1.2.0",
115 | "rimraf": "^2.7.1",
116 | "style-ext-html-webpack-plugin": "^4.1.2",
117 | "style-loader": "^0.23.1",
118 | "terser-webpack-plugin": "^1.4.1",
119 | "url-loader": "^2.1.0",
120 | "webpack": "^4.35.0",
121 | "webpack-cli": "^3.3.5"
122 | },
123 | "directories": {
124 | "server": "server"
125 | },
126 | "files": [
127 | "dist",
128 | "flamebird.js",
129 | "LICENSE",
130 | "postinstall.js",
131 | "README.md",
132 | "server"
133 | ],
134 | "bugs": {
135 | "url": "https://github.com/acacode/flamebird/issues"
136 | },
137 | "homepage": "https://github.com/acacode/flamebird"
138 | }
139 |
--------------------------------------------------------------------------------
/server/taskfile.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const resolve = require('path').resolve
3 | const _ = require('lodash')
4 | const commands = require('./utils/commands')
5 | const { TASK_RUNNERS } = require('./constants')
6 |
7 | let LIB_PATHS = null
8 |
9 | const createLibPaths = () => {
10 | const [utilFormat, utilDir] =
11 | process.platform === 'win32'
12 | ? ['.cmd', 'node_modules\\.bin\\']
13 | : ['', 'node_modules/.bin/']
14 | LIB_PATHS = _.reduce(
15 | fs
16 | .readdirSync(resolve('node_modules/.bin'))
17 | .filter(file => !file.match(/.*\.cmd$/)),
18 | (bin, name) => (bin[name] = `${utilDir}${name}${utilFormat}`) && bin,
19 | {}
20 | )
21 | }
22 |
23 | /**
24 | * load task files (package.json or Procfile)
25 | * @param {object} config
26 | * @param {string} procfilePath
27 | */
28 | function load(config, procfilePath) {
29 | const allCommands = []
30 | if (config.web || config.useOnlyPackageJson) {
31 | try {
32 | const packageJson = JSON.parse(fs.readFileSync('package.json').toString())
33 | config.name = packageJson.name || 'flamebird'
34 | _.forEach(packageJson.scripts, (command, taskName) => {
35 | if (!config.tasks.length || _.includes(config.tasks, taskName)) {
36 | allCommands.push(
37 | commands.createCommand(config.id, taskName, command, 'npm')
38 | )
39 | }
40 | })
41 | } catch (e) {
42 | console.warn('package.json not found')
43 | }
44 | }
45 | if (config.web || !config.useOnlyPackageJson) {
46 | try {
47 | const data = fs.readFileSync(procfilePath)
48 | _.each(_.compact(data.toString().split(/[\n\r]/g)), (line, i) => {
49 | if (line && line[0] !== '#') {
50 | const tuple = /^([A-Za-z0-9_-]+):\s*(.+)$/m.exec(line)
51 | const name = tuple[1].trim()
52 | const task = tuple[2].trim()
53 | if (!name || !task) {
54 | throw new Error(
55 | 'Syntax Error in Procfile, Line %d: No ' +
56 | (!tuple ? 'Procfile' : 'Command') +
57 | ' Found'
58 | )
59 | }
60 | if (!config.tasks.length || _.includes(config.tasks, name)) {
61 | allCommands.push(
62 | commands.createCommand(config.id, name, task, 'procfile')
63 | )
64 | }
65 | }
66 | })
67 | } catch (e) {
68 | console.warn('Procfile not found')
69 | }
70 | }
71 | console.log('allCommands', allCommands)
72 | return updateCommands(config, allCommands)
73 | }
74 |
75 | function setAbsolutePathsToTask(command, commandsWithoutPms) {
76 | const spaceChar = ' '
77 | const fixedTaskData = []
78 | const words = command.task.split(spaceChar)
79 | for (let x = 0; x < words.length; ) {
80 | const word = words[x]
81 | if (word === 'npm' || word === 'yarn') {
82 | const nextWordIsRun = words[x + 1] === 'run'
83 | const updatedCommand =
84 | commandsWithoutPms[words[x + (nextWordIsRun ? 2 : 1)]]
85 | if (updatedCommand) {
86 | fixedTaskData.push(updatedCommand)
87 | x += 2
88 | } else {
89 | fixedTaskData.push(word)
90 | x++
91 | }
92 | } else {
93 | fixedTaskData.push(LIB_PATHS[word] || word)
94 | x++
95 | }
96 | }
97 | return (command.rawTask = fixedTaskData.join(spaceChar))
98 | }
99 |
100 | function updateCommands(config, commands) {
101 | if (config.withoutTaskRunner) {
102 | if (LIB_PATHS === null) createLibPaths()
103 | _.reduce(
104 | _.sortBy(
105 | commands,
106 | ({ task }) => _.includes(task, 'yarn') || _.includes(task, 'npm')
107 | ),
108 | (commandsWithoutPms, command) => {
109 | // TODO: fix problem with displaying cross-env in .task property
110 | command.task = _.replace(command.task, 'cross-env ', '')
111 | commandsWithoutPms[command.name] = setAbsolutePathsToTask(
112 | command,
113 | commandsWithoutPms
114 | )
115 | return commandsWithoutPms
116 | },
117 | {}
118 | )
119 | } else {
120 | const taskRunner =
121 | TASK_RUNNERS[`${config.taskRunner}`.toUpperCase()] || config.taskRunner
122 | _.each(commands, command => {
123 | command.rawTask =
124 | command.type === 'procfile'
125 | ? command.task
126 | : `${taskRunner || command.type} ${command.name}`
127 | })
128 | }
129 |
130 | console.log('update Commands -> commands', commands)
131 | return config.sortByName ? _.sortBy(commands, 'name', 'asc') : commands
132 | }
133 |
134 | module.exports.load = load
135 |
--------------------------------------------------------------------------------
/client/scripts/WebLogger.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import AnsiUp from 'ansi_up'
3 | import _ from 'lodash'
4 | import {
5 | createSpan,
6 | toggleClass,
7 | createEl,
8 | createButton,
9 | createEnvsInput,
10 | } from '../helpers/dom_utils'
11 |
12 | export default class WebLogger {
13 | watchTaskLogsScrollTop = true
14 | autoScrollButton
15 | loggerId
16 | element
17 | ansiUp
18 |
19 | scrollTo(direction, animate, scrollPixels) {
20 | const scrollTop = scrollPixels
21 | ? $(this.element).scrollTop()
22 | : direction === 'bottom'
23 | ? this.element.scrollHeight
24 | : 0
25 | if (scrollPixels) {
26 | $(this.element).scrollTop(
27 | direction === 'bottom'
28 | ? scrollTop - scrollPixels
29 | : scrollTop + scrollPixels
30 | )
31 | } else if (animate) {
32 | $(this.element).animate({ scrollTop }, animate)
33 | } else {
34 | this.element.scrollTop = scrollTop
35 | }
36 | }
37 |
38 | createHTMLLog = logData => {
39 | const log = this.ansiUp.ansi_to_html(
40 | logData.replace(/\n/g, ' ').replace(/ /g, ' ')
41 | )
42 | if (log.includes('Warning:')) {
43 | return createSpan('ansi-yellow-fg', log)
44 | }
45 | if (log.includes('Exited Successfully')) {
46 | return createSpan('ended ok', log)
47 | }
48 | if (log.includes('Exited with exit code ')) {
49 | return createSpan('ended', log)
50 | }
51 | return log
52 | .replace(/(.<\/span>)/g, () =>
53 | createSpan('ansi-bright-black-fg mocha-test', '.')
54 | )
55 | .replace(/(!<\/span>)/g, () =>
56 | createSpan('ansi-red-fg mocha-test', '!')
57 | )
58 | }
59 |
60 | push(log, isRaw) {
61 | this.element.insertAdjacentHTML(
62 | 'beforeend',
63 | isRaw ? log : this.createHTMLLog(log)
64 | )
65 | }
66 |
67 | clear() {
68 | while (this.element.lastChild) {
69 | this.element.removeChild(this.element.lastChild)
70 | }
71 | }
72 |
73 | updateEnvs(envs) {
74 | if (_.keys(envs).length) {
75 | const container = document.createElement('div')
76 | container.classList.add('envs-log')
77 | container.innerHTML =
78 | _.map(
79 | envs,
80 | (value, key) =>
81 | `${createSpan('ansi-bright-magenta-fg', key)}=${createEnvsInput(
82 | key,
83 | value
84 | )}`
85 | ).join(', ') +
86 | createButton('logs-button', 'edit', 'global.enableEnvsForm()') +
87 | createButton('logs-button cancel', 'times', 'global.cancelEnvs()') +
88 | createButton('logs-button apply', 'check', 'global.updateEnvs()')
89 | this.element.appendChild(container)
90 | }
91 | }
92 |
93 | updateDescription(description) {
94 | const container = createEl('div', { className: 'task-data' })
95 | const span = createEl('span', { innerText: description })
96 | container.appendChild(span)
97 | this.element.appendChild(container)
98 | }
99 |
100 | onScroll() {
101 | toggleClass(this, 'scrolling', this.scrollTop > 70)
102 | }
103 |
104 | triggerScrollWatcher = () => {
105 | this.scrollTo('bottom', '1500')
106 | this.watchTaskLogsScrollTop = !this.watchTaskLogsScrollTop
107 | toggleClass(this.autoScrollButton, 'active', this.watchTaskLogsScrollTop)
108 | }
109 |
110 | createScrollActions(parent) {
111 | const scrollActions = createEl('div', {
112 | className: 'scroll-actions',
113 | parent,
114 | })
115 | createEl('button', {
116 | className: 'logs-button scroll-top',
117 | onclick: () => this.scrollTo('top', '500'),
118 | innerHTML: ' ',
119 | parent: scrollActions,
120 | })
121 | createEl('button', {
122 | className: 'logs-button clear',
123 | onclick: () => window.global.clearLogs(this.loggerId),
124 | innerHTML: ' ',
125 | parent: scrollActions,
126 | })
127 | this.autoScrollButton = createEl('button', {
128 | className: `logs-button autoscroll ${
129 | this.watchTaskLogsScrollTop ? 'active' : ''
130 | }`,
131 | onclick: () => this.triggerScrollWatcher(),
132 | innerHTML: ' ',
133 | parent: scrollActions,
134 | })
135 | }
136 |
137 | constructor(logsContainer, id) {
138 | this.loggerId = id
139 | const wrapper = createEl('div', {
140 | className: 'logs-wrapper',
141 | parent: logsContainer,
142 | })
143 | this.createScrollActions(wrapper)
144 | this.element = createEl('div', {
145 | className: 'logs-container',
146 | parent: wrapper,
147 | onscroll: this.onScroll,
148 | })
149 | this.ansiUp = new AnsiUp()
150 | this.ansiUp.use_classes = true
151 | this.ansiUp.escape_for_html = false
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-path-concat */
2 |
3 | const http = require('http')
4 | const path = require('path')
5 | const express = require('express')
6 | const bodyParser = require('body-parser')
7 | const opn = require('opn')
8 | const os = require('os')
9 | const _ = require('lodash')
10 | const memCache = require('./utils/mem_cache')
11 | const fs = require('fs')
12 | const pw = require('./processWorker')
13 | const { getCommandById } = require('./utils/commands')
14 | const { PATHS, MESSAGE_TYPES } = require('./constants')
15 | const kill = require('tree-kill')
16 | const { createWSConnection, sendMessage } = require('./ws')
17 | const { getMainConfig, getRC, updateRC, removeConfig } = require('./config')
18 |
19 | const rootPath = path.resolve(__dirname, PATHS.WEB_APP_ROOT)
20 |
21 | function start(config) {
22 | console.log('Initializing web server')
23 |
24 | const app = express()
25 |
26 | app.use(bodyParser.json())
27 | app.use(express.static(rootPath))
28 |
29 | app.get('/', (req, res) => res.sendFile(rootPath))
30 |
31 | app.get('/info', (req, res) => {
32 | const rcSnapshot = memCache.get('rc-snapshot')
33 |
34 | console.log('/info -> rcSnapshot', rcSnapshot)
35 |
36 | res.send({
37 | ...rcSnapshot,
38 | configs: _.map(rcSnapshot.configs, config => ({
39 | ..._.omit(config, 'pid', 'path'),
40 | commands: _.map(config.commands, command => ({
41 | ...command,
42 | logs: [],
43 | })),
44 | })),
45 | })
46 | })
47 |
48 | app.post('/refresh-rc-configs', (req, res) => {
49 | const freshRC = getRC()
50 | const rcSnapshot = memCache.get('rc-snapshot')
51 | const newConfig = freshRC.configs[freshRC.configs.length - 1]
52 | memCache.set('rc-snapshot', {
53 | configs: [...rcSnapshot.configs, newConfig],
54 | })
55 |
56 | kill(newConfig.pid, 'SIGINT')
57 | sendMessage(MESSAGE_TYPES.APPS_LIST_UPDATE, { ok: true })
58 | res.send('ok')
59 | return null
60 | })
61 |
62 | app.post('/:configId/:taskId/envs', (req, res) => {
63 | const { taskId, configId } = req.params
64 | const envs = req.body
65 |
66 | const currentCommand = getCommandById(configId, taskId)
67 | currentCommand.logs = []
68 | currentCommand.envs = envs
69 |
70 | pw.reRun(currentCommand)
71 |
72 | res.send('ok')
73 | })
74 |
75 | app.delete('/:configId', (req, res) => {
76 | const { configId } = req.params
77 | removeConfig(configId)
78 |
79 | res.send('ok')
80 | })
81 |
82 | app.delete('/:configId/:taskId/logs', (req, res) => {
83 | const { taskId, configId } = req.params
84 |
85 | getCommandById(configId, taskId).logs = []
86 |
87 | res.send('ok')
88 | })
89 |
90 | app.get('/:configId/:taskId/logs', (req, res) => {
91 | const { taskId, configId } = req.params
92 |
93 | res.send(getCommandById(configId, taskId).logs)
94 | })
95 |
96 | app.get('/project-version', (req, res) => {
97 | try {
98 | const packageJson = JSON.parse(fs.readFileSync('package.json').toString())
99 | res.send(packageJson.version || null)
100 | } catch (e) {
101 | res.send(null)
102 | }
103 | })
104 |
105 | app.post('/:configId/:taskId/run', (req, res) => {
106 | const { taskId, configId } = req.params
107 |
108 | const command = getCommandById(configId, taskId)
109 |
110 | pw.run(command)
111 |
112 | res.send('ok')
113 | })
114 |
115 | app.post('/:configId/:taskId/stop', (req, res) => {
116 | const { taskId, configId } = req.params
117 |
118 | const command = getCommandById(configId, taskId)
119 |
120 | pw.stop(command)
121 |
122 | res.send('ok')
123 | })
124 |
125 | const server = http.createServer(app)
126 |
127 | createWSConnection(server)
128 |
129 | server.listen(config.port, '0.0.0.0', () => {
130 | console.log(`Flamebird launched on port ${config.port}`)
131 | if (!config.withoutBrowser)
132 | try {
133 | opn('http://localhost:' + config.port, {
134 | app: os.platform() === 'win32' ? 'chrome' : 'google-chrome',
135 | })
136 | } catch (e) {
137 | console.error('Cannot open Google Chrome browser')
138 | }
139 | })
140 | }
141 |
142 | const update = config => {
143 | console.log('Try to connect to main flamebird process')
144 |
145 | const setConfigAsMain = () => {
146 | console.log('setConfigAsMain')
147 | updateRC(
148 | memCache.set('rc-snapshot', {
149 | configs: [
150 | _.merge(config, {
151 | main: true,
152 | }),
153 | ],
154 | })
155 | )
156 | start(config)
157 | }
158 |
159 | const mainConfig = getMainConfig()
160 |
161 | if (!mainConfig) {
162 | setConfigAsMain()
163 | return
164 | }
165 |
166 | process.once('uncaughtException', setConfigAsMain)
167 |
168 | http
169 | .request(
170 | {
171 | timeout: 5000,
172 | host: '127.0.0.1',
173 | path: `/refresh-rc-configs`,
174 | port: mainConfig.port,
175 | method: 'POST',
176 | },
177 | res => {
178 | res.resume()
179 | res.on('error', err => {
180 | console.log('TODO', err)
181 | })
182 | res.on('end', () => {
183 | console.log('Successfully connected')
184 | process.exit(0)
185 | })
186 | }
187 | )
188 | .end()
189 | }
190 |
191 | module.exports = {
192 | start,
193 | update,
194 | }
195 |
--------------------------------------------------------------------------------
/flamebird.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // Copyright IBM Corp. 2012,2015. All Rights Reserved.
4 | // Node module: flamebird
5 | // This file is licensed under the MIT License.
6 | // License text available at https://opensource.org/licenses/MIT
7 |
8 | const program = require('commander')
9 | const emitter = require('./server/utils/emitter')
10 | const { createConfig } = require('./server/config')
11 | const { yellow, red, grey, cyan } = require('./server/utils/colors')
12 | const processWorker = require('./server/processWorker')
13 | const server = require('./server/server')
14 |
15 | process.title = 'flamebird (nodejs task manager)'
16 |
17 | /* special properties for colorizing output logs */
18 | process.env.FORCE_COLOR = true
19 | process.env.colors = true
20 | process.env.color = true
21 |
22 | process.once('SIGINT', function() {
23 | emitter.emit('killall', 'SIGINT')
24 | process.exit()
25 | })
26 |
27 | // Kill All Child Processes & Exit on SIGTERM
28 | process.once('SIGTERM', function() {
29 | emitter.emit('killall', 'SIGTERM')
30 | process.exit()
31 | })
32 |
33 | program.on('--help', function() {
34 | console.log('\r\nExamples:')
35 | console.log(
36 | ' ' +
37 | grey('$') +
38 | ' fb start -p -t start:dev,server:dev ' +
39 | '- launch commands "start:dev" and "server:dev" (-t start:dev,server:dev) from package.json (-p)\r\n' +
40 | ' ' +
41 | grey('$') +
42 | ' fb start ' +
43 | '- launch all commands from Procfile and output logs to this command line\r\n' +
44 | ' ' +
45 | grey('$') +
46 | ' fb web ' +
47 | '- launch web GUI which have contained all possible commands from package.json etc.\r\n' +
48 | ' ' +
49 | grey('$') +
50 | ' fb web -w -u yarn ' +
51 | '- launch web GUI without opening new tab in the browser (-w) and will use another package manager (-u yarn) for launching commands from package.json\r\n' +
52 | ' ' +
53 | grey('$') +
54 | ' fb web -i ' +
55 | '- launch web GUI which will launch tasks without yarn or npm (using absolute paths: webpack -> node_modules/.bin/webpack) (-i)'
56 | )
57 | })
58 |
59 | program.version(getLogo(true), '-v, --version')
60 | program.usage('[command] [options]')
61 |
62 | program.option('-f, --procfile ', 'load procfile from file', 'Procfile')
63 | program.option(
64 | '-e, --env ',
65 | 'load environment from file, a comma-separated list',
66 | '.env'
67 | )
68 |
69 | program
70 | .command('start')
71 | .usage('[Options]')
72 | .option('-p, --package', 'Use package.json for managing tasks')
73 | .option(
74 | '-i, --ignore-trs',
75 | 'Allows to launch tasks without yarn or npm ( using absolute paths: webpack -> node_modules/.bin/webpack )',
76 | false
77 | )
78 | .option(
79 | '-t, --tasks [tasks]',
80 | 'List of tasks which will be run flamebird ( example : --tasks start,start:dev,start-server )'
81 | )
82 | .option(
83 | '-r, --task-runner ',
84 | `Allows to use another task runner for launch tasks. By default will use npm ( For example: -r yarn )`,
85 | 'npm'
86 | )
87 | .description(
88 | 'Launch commands from Procfile/package.json and output logs in the current command line'
89 | )
90 | .action(args => {
91 | const config = createConfig(args)
92 | processWorker.runAll(config.commands)
93 | })
94 |
95 | program
96 | .command('web')
97 | .usage('[Options]')
98 | .option('-p, --port ', 'sets the server port', 5050)
99 | .option(
100 | '-i, --ignore-trs',
101 | 'Allows to launch tasks without yarn or npm ( use absolute paths: webpack -> node_modules/.bin/webpack )',
102 | false
103 | )
104 | .option(
105 | '-n, --name ',
106 | 'Sets the project name. By default takes from "package.json" else "flamebird"',
107 | 'flamebird'
108 | )
109 | .option(
110 | '-t, --tasks [tasks]',
111 | 'List of tasks which will be run flamebird ( example : --tasks start,start:dev,start-server )'
112 | )
113 | .option(
114 | '-r, --task-runner ',
115 | `Allows to use another task runner for launch tasks. By default will use npm ( For example: -r yarn )`,
116 | 'npm'
117 | )
118 | .option(
119 | '-w, --without-browser',
120 | 'This option disable opening the new tab in Google Chrome browser',
121 | false
122 | )
123 | .option(
124 | '-s, --sort-by-name',
125 | 'This option using to sort all commands by name (asc)',
126 | false
127 | )
128 | .description(
129 | 'Launch ' +
130 | cyan('web application') +
131 | ' which will help to manage all commands from package.json/Procfile/Grunt/Gulp'
132 | )
133 | .action(args => {
134 | const config = createConfig(args, true)
135 | if (config.main) server.start(config)
136 | else server.update(config)
137 | })
138 |
139 | program.parse(process.argv)
140 |
141 | if (!process.argv.slice(2).length) {
142 | console.log(getLogo())
143 | program.outputHelp()
144 | }
145 |
146 | function getLogo(displayOnlyVersion) {
147 | const { version } = require('./package.json')
148 | const strings = []
149 | if (!displayOnlyVersion) {
150 | strings.push(yellow(' ╔══╗ ╔╗ ╔══╗ ╔╗ ╔╗ ╔═══╗ ╔══╗ ╔══╗ ╔═══╗ ╔══╗ '))
151 | strings.push(
152 | red(' ║╔═╝') +
153 | yellow(' ║║ ║╔╗║ ║║ ║║ ║╔══╝ ║╔╗║ ╚╗╔╝ ║╔═╗║ ') +
154 | red('║╔╗╚╗ ')
155 | )
156 | strings.push(
157 | red(' ║╚═╗ ') +
158 | yellow('║║ ║╚╝║ ║╚╗╔╝║ ║╚══╗ ║╚╝╚╗ ║║ ║╚═╝║') +
159 | red(' ║║╚╗║ ')
160 | )
161 | strings.push(
162 | red(' ║╔═╝ ║║ ║╔╗║') +
163 | yellow(' ║╔╗╔╗║ ║╔══╝ ║╔═╗║ ') +
164 | red('║║ ║╔╗╔╝ ║║ ║║ ')
165 | )
166 | strings.push(
167 | red(' ║║ ║╚═╗ ║║║║ ') +
168 | yellow('║║╚╝║║ ║╚══╗ ║╚═╝║') +
169 | red(' ╔╝╚╗ ║║║║ ║╚═╝║ ')
170 | )
171 | strings.push(
172 | red(' ╚╝ ╚══╝ ╚╝╚╝ ╚╝ ╚╝ ') +
173 | yellow('╚═══╝') +
174 | red(' ╚═══╝ ╚══╝ ╚╝╚╝ ╚═══╝ ')
175 | )
176 | strings.push(
177 | ' ' + grey(' - wonderful nodejs task manager ') + ' '
178 | )
179 | }
180 | const v =
181 | version +
182 | new Array(version.length >= 10 ? 10 : version.length - version.length).join(
183 | ' '
184 | )
185 | const commonSpace = displayOnlyVersion ? ' ' : ' '
186 | strings.push(commonSpace + red('╔═══════════════╗ '))
187 | strings.push(commonSpace + red('║') + yellow(' v' + v + ' ') + red('║ '))
188 | strings.push(commonSpace + red('╚═══════════════╝ ') + '\r\n')
189 | return strings.join('\r\n')
190 | }
191 |
--------------------------------------------------------------------------------
/client/scripts/TaskList.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import Tabs, { PRIORITY_TAB } from './Tabs'
3 | // import HotKeys from './HotKeys'
4 | import { toggleClass, addClass, removeClass, el } from '../helpers/dom_utils'
5 | import { createTab } from '../helpers/tabs'
6 | import WindowAttached from '../helpers/WindowAttached'
7 | import { createTaskElement } from '../helpers/taskList'
8 |
9 | const TASK_CLASSES = {
10 | ACTIVE: 'active',
11 | STOPPING: 'stopping',
12 | RUNNING: 'running',
13 | LAUNCHING: 'clicked',
14 | UPDATED: 'updated',
15 | }
16 |
17 | export default class TaskList extends WindowAttached('taskList') {
18 | element
19 | taskList = {}
20 | activeTaskByTab = {}
21 | notifyTaskTimers = {}
22 |
23 | constructor(
24 | taskListContainer,
25 | tasks,
26 | { onOpenTask, onRunTask, onStopTask, onCreateTaskEl } = {}
27 | ) {
28 | super()
29 | this.onOpenTask = onOpenTask || _.noop
30 | this.onRunTask = onRunTask || _.noop
31 | this.onStopTask = onStopTask || _.noop
32 | this.onStopTask = onStopTask || _.noop
33 | this.onCreateTaskEl = onCreateTaskEl || _.noop
34 |
35 | this.element = taskListContainer
36 |
37 | Tabs.listenChanges(this.handleTabChange)
38 | this.initalizeTasks(tasks)
39 | }
40 |
41 | initalizeTasks = tasks => {
42 | this.element.innerHTML = ''
43 |
44 | this.taskList = {}
45 | this.activeTaskByTab = {}
46 |
47 | this.taskList = _.reduce(
48 | tasks,
49 | (taskList, task) => {
50 | if (!taskList[task.type]) {
51 | taskList[task.type] = []
52 | }
53 | taskList[task.type].push(task)
54 | return taskList
55 | },
56 | // TODO: probably we can use just this.taskList here
57 | this.taskList
58 | )
59 |
60 | const tabs = _.map(_.keys(this.taskList), tabName => {
61 | this.activeTaskByTab[tabName] = {}
62 | return createTab(tabName, tabName === PRIORITY_TAB.name)
63 | })
64 |
65 | Tabs.createTabs(tabs)
66 |
67 | let activeTab = null
68 |
69 | for (let x = 0; x < tabs.length; x++) {
70 | const tab = tabs[x]
71 | if (tab) {
72 | const tasks = this.getTasksByTab(tab)
73 | if (tasks && tasks.length) {
74 | this.activeTaskByTab[tab.name] = [tasks[0].id]
75 | if (!activeTab || tab.name === PRIORITY_TAB.name) {
76 | activeTab = tab
77 | }
78 | } else {
79 | delete this.taskList[tab.name]
80 | delete this.activeTaskByTab[tab.name]
81 | Tabs.removeTab(tab)
82 | x--
83 | }
84 | }
85 | }
86 |
87 | if (activeTab) Tabs.setActive(activeTab.name)
88 | }
89 |
90 | handleTabChange = name => {
91 | this.clear()
92 | this.updateTaskList(name)
93 |
94 | setTimeout(() => {
95 | const { name: currentName } = Tabs.getActive()
96 | if (name !== currentName) {
97 | name = currentName
98 | }
99 | if (this.activeTaskByTab[name].length) {
100 | el(`#${this.activeTaskByTab[name][0]}`).click()
101 | } else {
102 | global.getLogger().clear()
103 | }
104 | }, 0)
105 | }
106 |
107 | updateTaskList(tabName) {
108 | _.forEach(this.taskList[tabName], (task, index) => {
109 | // TODO: ???
110 | // if (HotKeys.isEnabled) {
111 | // HotKeys.connect(task, index)
112 | // }
113 | const taskEl = createTaskElement(task, index, {
114 | onOpenTask: () => this.onOpenTask(task),
115 | onRunTask: () => this.onRunTask(task),
116 | onStopTask: () => this.onStopTask(task),
117 | })
118 |
119 | this.onCreateTaskEl(taskEl, index)
120 |
121 | this.element.appendChild(taskEl)
122 | })
123 | }
124 |
125 | getTask = id => _.find(_.concat.apply(null, _.values(this.taskList)), { id })
126 |
127 | updateTask(id, { isRun, isActive, isLaunching, isStopping }) {
128 | const task = this.getTask(id)
129 |
130 | // TODO: fix task as undefined
131 |
132 | const taskElem = document.getElementById(task.id)
133 | task.isRun = isRun
134 | task.isLaunching = !!isLaunching
135 | task.isStopping = !!isStopping
136 | task.isActive = !!isActive
137 |
138 | if (task.isActive) {
139 | const prevActiveTask = document.querySelector(
140 | `.task.${TASK_CLASSES.ACTIVE}`
141 | )
142 | if (prevActiveTask) {
143 | const prevTaskId = this.activeTaskByTab[task.type][0]
144 | if (prevTaskId !== task.id) {
145 | this.getTask(prevTaskId).isActive = false
146 | removeClass(prevActiveTask, TASK_CLASSES.ACTIVE)
147 | }
148 | }
149 | this.activeTaskByTab[task.type] = [task.id]
150 | addClass(taskElem, TASK_CLASSES.ACTIVE)
151 | }
152 | toggleClass(taskElem, TASK_CLASSES.STOPPING, isStopping)
153 | toggleClass(taskElem, TASK_CLASSES.RUNNING, isRun)
154 | toggleClass(taskElem, TASK_CLASSES.LAUNCHING, isLaunching)
155 | return task
156 | }
157 |
158 | clear() {
159 | while (this.element.lastChild)
160 | this.element.removeChild(this.element.lastChild)
161 | }
162 |
163 | setActive(task, isLaunching, isStopping) {
164 | this.updateTask(task.id, {
165 | isRun: task.isRun,
166 | isActive: true,
167 | isLaunching: isLaunching === undefined ? task.isLaunching : isLaunching,
168 | isStopping: isStopping === undefined ? task.isStopping : isStopping,
169 | })
170 | }
171 |
172 | getTasksByTab = tab => {
173 | const tasks = this.taskList[tab.name]
174 | return tasks && tasks.length && tasks
175 | }
176 |
177 | getAllFromActiveTab(filter) {
178 | const tasks = this.getTasksByTab(Tabs.getActive())
179 | return filter ? _.filter(tasks, filter) : tasks
180 | }
181 |
182 | getActive = () => this.getTask(this.activeTaskByTab[Tabs.getActive().name][0])
183 |
184 | notifyAboutTask(taskId, isUpdated, options) {
185 | const task = this.getTask(taskId)
186 | const activeTab = Tabs.getActive()
187 | if (!options) options = {}
188 | if (task.type === activeTab.name)
189 | el(`#${taskId}`).classList[isUpdated ? 'add' : 'remove'](
190 | TASK_CLASSES.UPDATED
191 | )
192 | if (Notification && options.notify && isUpdated) {
193 | if (this.notifyTaskTimers[taskId]) {
194 | clearTimeout(this.notifyTaskTimers[taskId])
195 | this.notifyTaskTimers[taskId] = null
196 | }
197 | this.notifyTaskTimers[taskId] = setTimeout(() => {
198 | const task = this.getTask(taskId)
199 | if (Notification.permission === 'granted') {
200 | const notification = new Notification(
201 | `"${task.name}" has been updated`,
202 | {
203 | icon: 'assets/logo2_small.png',
204 | body:
205 | `project: ${options.projectName}\r\n` +
206 | `taskfile: [${task.type}]\r\n` +
207 | 'latest message: \r\n' +
208 | `...${options.log
209 | .split('')
210 | .splice(-22)
211 | .join('')}`,
212 | }
213 | )
214 | notification.onclick = () => window.focus()
215 | } else Notification.requestPermission()
216 | }, 2800)
217 | }
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
🔥 flamebird 🔥
12 |
13 | the nodejs task manager for Procfile-based or npm-based applications
14 |
15 |
16 |
17 | ## 🚀 Installation
18 |
19 | $ npm install -g flamebird
20 |
21 | ## 📄 Usage
22 |
23 | To start Flamebird you can use `fb [command] [options]` (or longer alias `flamebird [command] [options]`).
24 | Application provides two commands: `fb start` and `fb web` (read below).
25 |
26 | Need help? Use command:
27 |
28 | $ fb --help
29 | # or simply
30 | $ fb
31 |
32 | ## 💻 Console version (`fb start`)
33 |
34 | $ fb start [options]
35 |
36 | Run tasks from `Procfile` or `package.json`
37 |
38 | Options:
39 | - `-p, --package` - using package.json for the managing tasks. (:warning: with this option the command `start` run all tasks from `package.json`, for resolving it , please use option `-t`)
40 | - `-t, --tasks [tasks]` - list of tasks which needs to async run in `fb start` ( example : `fb start --tasks start,start:dev,start-server` and then tasks are `start`,`start:dev`,`start-server` will have been runned asynchronously )
41 |
42 | ## 💻 Web version (`fb web`)
43 |
44 | $ fb web [options]
45 |
46 | Launch web-application which is task-manager. That command has more abilities than `start`. Web-application is reading `Procfile` and `package.json` and adding ability to launch scripts inside this files together
47 |
48 | Options:
49 | - `-t, --tasks [tasks]` - list of tasks which will be managing in the `fb web` command ( example : `fb web --tasks start,start:dev,start-server` and this tasks will be showing in the web-application `start`,`start:dev`,`start-server` )
50 | - `-p, --port ` - sets the server port, by default `5050`
51 | - `-n, --name ` - sets the project name. Display name of the project in title and header. By default using name of project inside `package.json` otherwise `flamebird`
52 |
53 | hotkeys
54 |
55 | hotkeys works only if  is triggered.
56 |
57 | hotkey | action
58 | ------------ | -------------
59 | Q ,W ,E ...M ,< ,> ,/ | Open task which assigned to specific key. 
60 | SHIFT + R | Run/Stop selected task.
61 | TAB | Switch between `Procfile` and `package.json` tabs
62 | DEL | Clear logs in selected task
63 | ↑ | Partially scroll up logs in selected task
64 | ↓ | Partially scroll down logs in selected task
65 | SHIFT + ↑ | Fully scroll up logs in selected task
66 | SHIFT + ↓ | Fully scroll down logs in selected task
67 | SHIFT + A | Run all tasks
68 | SHIFT + S | Stop all tasks
69 |
70 |
71 | How it looks:
72 |
73 | 
74 |
75 |
76 |
77 | ## Contribution
78 |
79 |
80 | If you want to help this project you need to read this part of readme.md for more detail understanding of for what some things are needed.
81 |
82 | First of all take a look at project structure:
83 |
84 | - **flamebird project**
85 | - [flamebird.js](./flamebird.js) - Executable by nodejs file when call `fb start`/`fb web`. Describe information about commands `web` and `start`.
86 | - [LICENSE](./LICENSE)
87 | - [nodemon.json](./nodemon.json) - Nodemon config file. Only needed for debug flamebird.
88 | - [node_modules](./node_modules) - you know what is that :D
89 | - [package-lock.json](./package-lock.json)
90 | - [package.json](./package.json)
91 | - [postinstall.js](./postinstall.js) - Script which execute after installing flamebird dependencies
92 | - [Procfile](./Procfile) - List of scripts which execute via nodemon. Only needed for debug flamebird.
93 | - [README.md](./README.md)
94 | - [webpack.config.js](./webpack.config.js)
95 | - [babel.config.js](./babel.config.js)
96 | - [CHANGELOG.md](./CHANGELOG.md)
97 | - __assets__
98 | - [task_button.png](./assets/task_button.png)
99 | - [hotkeys_button.png](./assets/hotkeys_button.png)
100 | - [web-ui-screen.png](./assets/web-ui-screen.png)
101 | - __client__ Client application ⚡️
102 | - __controllers__ - folder with controllers where each one have specific UI manipulations
103 | - [Header.js](./client/controllers/Header.js)
104 | - __assets__ - folder with assets
105 | - [logo.psd](./client/assets/logo.psd) - source of the flamebird logo
106 | - [logo.png](./client/assets/logo.png)
107 | - [logo2_small.png](./client/assets/logo2_small.png)
108 | - [logo2.png](./client/assets/logo2.png)
109 | - [logo2_transparent.png](./client/assets/logo2_transparent.png)
110 | - __helpers__ - folder with helpers where helper contains specific logic. In future it will be removed or moved into modules in `scripts` folder.
111 | - [dom_utils.js](./client/helpers/dom_utils.js)
112 | - [hotKeys.js](./client/helpers/hotKeys.js)
113 | - [tabs.js](./client/helpers/tabs.js)
114 | - [taskList.js](./client/helpers/taskList.js)
115 | - [WindowAttached.js](./client/helpers/WindowAttached.js)
116 | - [global.js](./client/global.js) - global file which implement pattern Facade and combine all interactions of modules from `scripts` folder.
117 | - [index.html](./client/index.html)
118 | - [medium-screens.css](./client/medium-screens.css)
119 | - __scripts__ - Bunch of modules which needed for web application.
120 | - [Api.js](./client/scripts/Api.js) - Contains all API endpoints which needed for client. It uses `kinka` as http web client.
121 | - [Configs.js](./client/scripts/Configs.js) - Responsible for update configs list
122 | - [HotKeys.js](./client/scripts/HotKeys.js) - Hot keys
123 | - [Tabs.js](./client/scripts/Tabs.js) - Module which responsible for update tabs list.
124 | - [TaskList.js](./client/scripts/TaskList.js) - Module which responsible for update tasks list.
125 | - [ThemeSwitcher.js](./client/scripts/ThemeSwitcher.js) - Module which responsible for theme switching.
126 | - [WebLogger.js](./client/scripts/WebLogger.js) - Logger module. Output logs into the logs container
127 | - [WebSocket.js](./client/scripts/WebSocket.js) - WebSocket client connection
128 | - [small-screens.css](./client/small-screens.css)
129 | - __styles__
130 | - [dark-theme.css](./client/styles/dark-theme.css)
131 | - [fullscreen.css](./client/styles/fullscreen.css)
132 | - [hot_keys-shortcuts.css](./client/styles/hot_keys-shortcuts.css)
133 | - [styles.css](./client/styles.css)
134 | - __dist__ - client build folder
135 | - __server__ Server application ⚡️
136 | - [config.js](./server/config.js) - module which working with configuration file (`.flamebirdrc`)
137 | - [constants.js](./server/constants.js) - module which contains constant values. It usings on both sides (client, server)
138 | - [processWorker.js](./server/processWorker.js) - module which responsible for run/stop tasks
139 | - [server.js](./server/server.js) - [`Web version`] module which contains API endpoints.
140 | - __utils__ - bunch of utilities 😊
141 | - [colors.js](./server/utils/colors.js) - tools which colorize console output
142 | - [commands.js](./server/utils/commands.js) - tools which needed for CRUD commands
143 | - [emitter.js](./server/utils/emitter.js) - instance of `events.EventEmitter`.
144 | - [envs.js](./server/utils/envs.js) - tools which needed for parse environment variables
145 | - [mem_cache.js](./server/utils/mem_cache.js) - just simple object which save all information. Have methods `get`, `set`
146 | - [processes.js](./server/utils/processes.js) - tools which needed for create and kill processes
147 | - [ws.js](./server/ws.js) - [`Web version`] Websocket connection
148 | - [taskfile.js](./server/taskfile.js) - module which parse all commands from `Procfile` and `package.json`
149 |
150 |
151 |
152 |
153 |
154 | ## 📝 License
155 |
156 | Licensed under the [MIT License](./LICENSE).
157 |
158 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # Changelog
3 |
4 |
5 | ## [3.0.0-alpha] - {date}
6 |
7 | Improvenments:
8 | - command `fb web` will have only one instance of NodeJS application.
9 | First call `fb web -p SOME_PORT` create a server hosted on port `SOME_PORT`.
10 | But call `fb web` inside another project will not create a new server
11 | But instead of this you can see all your running projects looking at `localhost:SOME_PORT`
12 |
13 |
14 | ### Added
15 | - `web` [internal] changed webpack configuration for web client
16 | - `web` new options:
17 | - `-i, --ignore-trs` (in previous version it used by default)
18 | Allows to launch tasks without yarn or npm ( use absolute paths: webpack -> node_modules/.bin/webpack )
19 | default: `false`
20 | - `-r, --task-runner `
21 | Allows to use another task runner for launch tasks. By default will use npm ( For example: -r yarn )
22 | default: `'npm'`
23 | - `-w, --without-browser`
24 | This option disable opening the new tab in Google Chrome browser
25 | default: `false`
26 | - `-s, --sort-by-name`
27 | This option using to sort all commands by name (asc)
28 | default: `false`
29 | - `cmd` new options:
30 | - `-i, --ignore-trs` (in previous version it used by default)
31 | Allows to launch tasks without yarn or npm ( use absolute paths: webpack -> node_modules/.bin/webpack )
32 | default: `false`
33 | - `-r, --task-runner `
34 | Allows to use another task runner for launch tasks. By default will use npm ( For example: -r yarn )
35 | default: `'npm'`
36 |
37 | ### Changed
38 | - [internal] fully refactored code on nodeJS and web view
39 | - Option prefix from `-j, --procfile ` to `-f, --procfile `
40 | - [web] changed API endpoints:
41 | - `POST:/run/${taskId}` -> `POST:/${configId}/${taskId}/run`
42 | - `POST:/stop/${taskId}` -> `POST:/${configId}/${taskId}/stop`
43 | - `POST:/clear-logs/${taskId}` -> `DELETE/${configId}/${taskId}/logs`
44 | - `GET:/logs/${taskId}` -> `GET:/${configId}/${taskId}/logs`
45 | - `POST:/update-envs({ id: string, envs: object})` -> `PUT:/${configId}/${taskId}/envs(envs:object)`
46 |
47 |
48 | ### Fixed
49 | - Problems linked with running flamebird without `node_modules` folder
50 |
51 | ### Removed
52 | - Removed default usage experimental converting npm/yarn run scripts to the absolute paths
53 | - [web]
54 | - `/run-all` API endpoint
55 | - `/stop-all` API endpoint
56 |
57 |
58 | ## [2.1.4] - 12/06/2018
59 |
60 | ### Added
61 | - `web` hotkey feautures
62 | - DEL Clear logs in selected task
63 | - ↑ Partially scroll up logs in selected task
64 | - ↓ Partially scroll down logs in selected task
65 | - SHIFT + A Run all tasks
66 | - SHIFT + S Stop all tasks
67 | - SHIFT + ↑ Fully scroll up logs in selected task
68 | - SHIFT + ↓ Fully scroll down logs in selected task
69 | - `web` browser notifications about updates of the running tasks
70 | - `web` mobile version
71 |
72 | ### Changed
73 | - added emojis to `README.md`
74 | - added documentation for hotkeys
75 |
76 |
77 | ### Fixed
78 | - typo fixes in `README.md`
79 |
80 |
81 | ## [2.0.0] - 12/01/2018
82 |
83 | ### Changed
84 | - `web` replaced web http-client `axios` to `kinka`
85 |
86 | ### Fixed
87 | - `web` big line height between lines
88 | - `web` unique task ids
89 | - `web` unique websocket session ids
90 |
91 | ## [1.8.6]
92 |
93 | ### Changed
94 | - `web` - [Internal] - migrated client code to ES6
95 |
96 | ### Fixed
97 | - bug - [Cannot find module 'commander'](https://github.com/acacode/flamebird/issues/49)
98 |
99 | ## [1.8.5]
100 |
101 | ### Changed
102 | - replaced `var` operator to ES6 `const`/`let`
103 |
104 | ### Fixed
105 | - text corrections in `README.md`
106 | - removed not needed dependencies and updated project version in `package-lock.json`
107 |
108 | ## [1.8.4--fix]
109 |
110 | ### Fixed
111 | - bugs with tasks using `cross-env` utility
112 |
113 | ## [1.8.3--fix]
114 |
115 | ### Fixed
116 | - `web` - bug with `Keyboard.connect is not a function`
117 | - absolute paths for npm tasks
118 |
119 | ## [1.8.02]
120 |
121 | ### Fixed
122 | - `web` - a small fixes in layout of the web page
123 |
124 | ## [1.8.0]
125 |
126 | ### Fixed
127 | - maximum `killall` event listeners. The removing of the `killall` event listener if task is completed
128 | - `web` - more than one websocket clients
129 | - `web` - restart of the task has been fixed
130 | - `start` - not all processes was launched from Procfile using `start` command
131 |
132 |
133 | ### Changed
134 | - `web` - a small UI/UX fixes.
135 |
136 | ### Removed
137 | - `web` - `blue` theme
138 |
139 | ## [1.7.91 - 1.7.94]
140 |
141 | ### Fixed
142 | - `web` - Fix displaying logs for non active task in the active task
143 | - `web` - Fix problem with changing env variables in web (previous process not killed)
144 | - Remove unsupportable spread operator
145 |
146 | ## [1.7.9]
147 |
148 | ### Fixed
149 | - `web` - Cannot switch to `npm` tab [32 issue](https://github.com/acacode/flamebird/issues/32)
150 | - `web` - Cannot read property `isActive` of undefined [30 issue](https://github.com/acacode/flamebird/issues/30)
151 |
152 | ## [1.7.8]
153 |
154 | ### Added
155 | - Running commands without `yarn`/`npm run`. Launching libraries in the commands via full path to library (util -> node_modules/.bin/util)[14 issue](https://github.com/acacode/flamebird/issues/14) & [25 issue](https://github.com/acacode/flamebird/issues/25)
156 | - Integration with Travis CI [17 issue](https://github.com/acacode/flamebird/issues/17)
157 |
158 | ### Fixed
159 | - `web` - Fixed whitespace in the logs [26 issue](https://github.com/acacode/flamebird/issues/26)
160 |
161 | ## [1.7.5]
162 |
163 | ### Added
164 | - Ability of the parsing and displaying `.env` file [20 issue](https://github.com/acacode/flamebird/issues/20)
165 |
166 | ### Changed
167 | - [Internal] FE/BE code refactoring
168 | - `web` - changes in webpack config. Add builds for `*.css` files
169 | - add post install script ( only decorations )
170 | - `web` - Fix styles for small screens and for fullscreen mode.
171 |
172 | ### Fixed
173 | - Displaying logs not linked with opened task in web [21 issue](https://github.com/acacode/flamebird/issues/21)
174 |
175 | ## [1.7.1 - 1.7.31]
176 |
177 | ### Fixed
178 | - fixed webpack build after installing npm packages
179 |
180 | ## [1.7.0] - 13/05/2018
181 |
182 | ### Changed
183 | - refactoring code in frontend part of application
184 | - update styles for web application
185 |
186 | ### Fixed
187 | - loosing colors in logs [18 issue](https://github.com/acacode/flamebird/issues/18)
188 | - problems with running webpack-dev-server in flamebird [15 issue](https://github.com/acacode/flamebird/issues/15)
189 |
190 | ### Added
191 | - `web` - added hotkeys button. Which helps task switching or tabs switching via keyboard
192 | - `web` - added themes [default(white), dark, blue]
193 | - `web` - added fullscreen button. Changes task runner window size
194 | - `web` - webpack build
195 | - `web` - create flamebird logo [16 issue](https://github.com/acacode/flamebird/issues/16)
196 | - `web` - ability of the renaming values of the env variables [13 issue](https://github.com/acacode/flamebird/issues/13)
197 |
198 | ## [1.6.7] -
199 |
200 | ### Changed
201 | - renaming option `-P` to `-p` in the `web` command, for the setting port
202 | - update styles for web application
203 |
204 | ### Added
205 | - `web` - added autoscroll button on the right bottom side. For turning on/off autoscrolling logs to bottom
206 | - `web` - [dev only] function for colorizing specific words like so `[emitted]`, `[built]` etc.
207 | - `web` - flamebird can read and procfile and package.json together, and added ability switching between this files in the web-application
208 |
209 | ### Removed
210 | - removed option `-p, --package`
211 |
212 | ## [1.5.5] - 04/23/2018
213 |
214 | ### Changed
215 | - working status of the tasks dependent on the server's status of task
216 | - updated README.md
217 |
218 | ### Fixed
219 | - Normalized showing logs of the task
220 | - Envs isn't passing to the command [BUG](https://github.com/acacode/flamebird/issues/3)
221 | - fb web: UnhandledPromiseRejectionWarning: Error: spawn chrome ENOENT [BUG](https://github.com/acacode/flamebird/issues/2)
222 |
223 |
224 | ## [1.5.3] - 04/21/2018
225 |
226 | ### Added
227 | - option `-n, --name ` for the `web` command. Sets the name of application
228 | - added feature of the opening new tab of Google Chrome browser when we launch `web` command
229 |
230 | ### Changed
231 | - update styles for web application
232 | - route `commands` renamed to `info` and now returns object with properties `appName` and `commands`
233 |
234 |
235 | ## [1.5.0] - 04/21/2018
236 |
237 | ### Added
238 | - `fb` - additional command name for the calling flamebird
239 | - command `flamebird web` - launch webview of flamebird application and working with all processes from webview
240 | - option `-p, --package` for commands `start` and `web` which needs for using `package.json` as the managing tasks instead of `Procfile`
241 | - option `-t, --tasks [tasks]` for commands `start` and `web` which needs for setting specific tasks which needs to the working
242 | - option `-P, --port ` for `web` command. Sets the server port. By default 5050 value
243 |
244 |
245 | ## [1.0.0] - 04/18/2018
246 |
247 | ### Added
248 | - command `flamebird start` for the launching all commands in Procfile
249 | - option `-j, --procfile ` for the loading `Procfile` from `` . by default using `./Procfile` path
--------------------------------------------------------------------------------
/client/global.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import $ from 'jquery'
3 | import api from './scripts/Api'
4 | import TaskList from './scripts/TaskList'
5 | import WebLogger from './scripts/WebLogger'
6 | import HotKeys from './scripts/HotKeys'
7 | import { el as getEl } from './helpers/dom_utils'
8 | import Tabs from './scripts/Tabs'
9 | import WindowAttached from './helpers/WindowAttached'
10 | import { clearifyEvent } from './helpers/hotKeys'
11 | import { Header } from './controllers/Header'
12 | import WebSocket from './scripts/WebSocket'
13 | import ConfigsManager from './scripts/Configs'
14 |
15 | export const DEFAULT_PROJECT_NAME = 'flamebird'
16 |
17 | class Global extends WindowAttached('global') {
18 | watchTaskLogsScrollTop = true
19 | setTheme
20 | previousEnvs = null
21 | projectName = DEFAULT_PROJECT_NAME
22 | pageIsNotActive = false
23 |
24 | configsManager = new ConfigsManager('.configs-list', {
25 | // TODO: fix config as undefined
26 | onSetConfig: config => this.handleConfigSet(config),
27 | getConfigs: async () => {
28 | const {
29 | data: { configs },
30 | } = await api.getProjectInfo()
31 | return configs || []
32 | },
33 | onRemoveConfig: config => {
34 | const activeConfig = this.configsManager.getActiveConfig()
35 |
36 | // TODO: fix activeConfig as undefined
37 | if (activeConfig.id === config.id) {
38 | this.configsManager.setConfig(0)
39 | }
40 | this.stopAllTasks(config)
41 | api.removeConfig(config.id)
42 | },
43 | })
44 |
45 | logger
46 | taskList = null
47 | hotkeys = new HotKeys({
48 | tab: event => {
49 | clearifyEvent(event)
50 | Tabs.setNextAsActive()
51 | return false
52 | },
53 | arrowUp: () => this.logger.scrollTo('bottom', 0, 40),
54 | arrowDown: () => this.logger.scrollTo('top', 0, 40),
55 | del: () => this.clearLogs(),
56 | shiftA: () => this.runAllTasks(),
57 | shiftS: () => this.stopAllTasks(),
58 | shiftR: () => {
59 | const activeTask = this.taskList.getActive()
60 |
61 | if (!activeTask.isLaunching) {
62 | if (activeTask.isRun) {
63 | this.stopTask(activeTask)
64 | } else {
65 | this.runTask(activeTask)
66 | }
67 | }
68 | },
69 | shiftArrowUp: () => this.logger.scrollTo('top', '1500'),
70 | shiftArrowDown: () => this.logger.scrollTo('bottom', '500'),
71 | })
72 |
73 | header = new Header({ hotkeys: this.hotkeys })
74 |
75 | showTaskList() {
76 | $(document.body).toggleClass('task-list-showed')
77 | }
78 |
79 | handleConfigSet = ({ commands, name }) => {
80 | this.setProjectName(name)
81 |
82 | if (this.taskList) {
83 | this.taskList.initalizeTasks(commands)
84 | } else {
85 | this.taskList = new TaskList(getEl('#task-list'), commands, {
86 | onOpenTask: this.openTask,
87 | onRunTask: this.runTask,
88 | onStopTask: this.stopTask,
89 | onCreateTaskEl: this.onCreateTaskEl,
90 | })
91 | }
92 | }
93 |
94 | runAllTasks(config = this.configsManager.getActiveConfig()) {
95 | _.each(
96 | this.taskList.getAllFromActiveTab({ isRun: false }),
97 | ({ isRun, id, isActive }) => {
98 | this.taskList.updateTask(id, { isRun, isActive, isLaunching: true })
99 | api.runTask(config.id, id)
100 | }
101 | )
102 | }
103 |
104 | stopAllTasks(config = this.configsManager.getActiveConfig()) {
105 | _.each(
106 | this.taskList.getAllFromActiveTab({ isRun: true }),
107 | ({ isRun, id, isActive }) => {
108 | this.taskList.updateTask(id, { isRun, isActive, isStopping: true })
109 | api.stopTask(config.id, id)
110 | }
111 | )
112 | }
113 |
114 | clearLogs(activeTask) {
115 | if (!activeTask) {
116 | activeTask = this.taskList.getActive()
117 | }
118 | if (_.isString(activeTask)) {
119 | activeTask = this.taskList.getTask(activeTask)
120 | }
121 | const config = this.configsManager.getActiveConfig()
122 | api.clearLogs(config.id, activeTask.id)
123 | activeTask.logs = []
124 | this.logger.clear()
125 | this.logger.updateDescription(activeTask.task)
126 | this.logger.updateEnvs(activeTask.envs)
127 | }
128 |
129 | triggerScrollWatcher(e) {
130 | this.logger.scrollTo('bottom', '1500')
131 | this.watchTaskLogsScrollTop = !this.watchTaskLogsScrollTop
132 | $('.logs-button.autoscroll').toggleClass(
133 | 'active',
134 | this.watchTaskLogsScrollTop
135 | )
136 | }
137 |
138 | handleClickTab(tab) {
139 | if (tab !== Tabs.getActive().name) {
140 | Tabs.setActive(tab)
141 | }
142 | }
143 |
144 | enableEnvsForm() {
145 | getEl('.envs-log').classList.add('active')
146 | this.previousEnvs = _.clone(this.taskList.getActive().envs)
147 | }
148 |
149 | updateEnvs() {
150 | getEl('.envs-log').classList.remove('active')
151 | const activeTask = this.taskList.getActive()
152 | _.each(getEl('.envs-log > input', true), el => {
153 | const input = $(el)
154 | this.previousEnvs[input.attr('key')] = input.val()
155 | })
156 | activeTask.envs = _.clone(this.previousEnvs)
157 | this.clearLogs(activeTask)
158 | const config = this.configsManager.getActiveConfig()
159 | api.updateEnvs(config.id, activeTask.id, this.previousEnvs)
160 | this.taskList.updateTask(activeTask.id, {
161 | isRun: true,
162 | isActive: activeTask.isActive,
163 | isLaunching: true,
164 | })
165 | this.previousEnvs = null
166 | }
167 |
168 | cancelEnvs() {
169 | getEl('.envs-log').classList.remove('active')
170 | _.each(getEl('.envs-log > input', true), el => {
171 | const input = $(el)
172 | input.val(this.previousEnvs[input.attr('key')])
173 | })
174 | this.taskList.getActive().envs = _.clone(this.previousEnvs)
175 | this.previousEnvs = null
176 | }
177 |
178 | async updateTaskLogs({ task, envs, id }) {
179 | this.logger.clear()
180 | const config = this.configsManager.getActiveConfig()
181 | const { data: rawLogs } = await api.getLogs(config.id, id)
182 | this.logger.updateDescription(task)
183 | this.logger.updateEnvs(envs)
184 | const logs = _.map(rawLogs, this.logger.createHTMLLog).join('')
185 | setTimeout(() => {
186 | this.logger.push(logs, true)
187 | this.logger.scrollTo('bottom')
188 | }, 0)
189 | }
190 |
191 | openTask = task => {
192 | // const active = this.taskList.getActive()
193 | // if (!active || id !== active.id) {
194 | if (task.id) {
195 | if (this.header.notificationsEnabled)
196 | this.taskList.notifyAboutTask(task.id, false)
197 | this.taskList.setActive(task)
198 | this.updateTaskLogs(task)
199 | }
200 | // }
201 | }
202 |
203 | runTask = task => {
204 | window.event.stopPropagation()
205 | // const task = this.taskList.getTask(id)
206 | if (!task.isLaunching && !task.isRun) {
207 | const config = this.configsManager.getActiveConfig()
208 | this.taskList.setActive(task, true)
209 | this.updateTaskLogs(task)
210 | api.runTask(config.id, task.id)
211 | }
212 | }
213 |
214 | stopTask = task => {
215 | window.event.stopPropagation()
216 | // const task = this.taskList.getTask(id)
217 | if (!task.isLaunching && task.isRun) {
218 | const config = this.configsManager.getActiveConfig()
219 | this.taskList.updateTask(task.id, {
220 | isActive: task.isActive,
221 | isStopping: true,
222 | })
223 | api.stopTask(config.id, task.id)
224 | }
225 | }
226 |
227 | onCreateTaskEl = (taskEl, index) => {
228 | if (this.hotkeys.isEnabled) {
229 | this.hotkeys.connectTaskButton(taskEl, index)
230 | }
231 | }
232 |
233 | handleOnUpdateLog = ({ name, id, isRun, isLaunching, isStopping, log }) => {
234 | if (name) {
235 | const isActive = id === this.taskList.getActive().id
236 | this.taskList.updateTask(id, {
237 | isRun,
238 | isActive,
239 | isLaunching,
240 | isStopping,
241 | })
242 | if (log) {
243 | if (isActive) {
244 | this.logger.push(log)
245 | if (this.watchTaskLogsScrollTop) this.logger.scrollTo('bottom')
246 | if (this.header.notificationsEnabled && this.pageIsNotActive) {
247 | this.taskList.notifyAboutTask(id, true, {
248 | notify: this.header.notificationsEnabled,
249 | projectName: this.projectName,
250 | log,
251 | })
252 | }
253 | } else if (this.header.notificationsEnabled) {
254 | this.taskList.notifyAboutTask(id, true, {
255 | notify: this.header.notificationsEnabled,
256 | log,
257 | projectName: this.projectName,
258 | })
259 | }
260 | }
261 | }
262 | }
263 |
264 | getLogger = () => this.logger
265 | getTaskList = () => this.taskList
266 |
267 | setProjectName = name => {
268 | this.projectName = name
269 | if (this.projectName) {
270 | $('title').text(`${this.projectName} | fb`)
271 | $('.title span').text(this.projectName)
272 | }
273 | }
274 |
275 | constructor() {
276 | super()
277 | $(document).ready(async () => {
278 | $(window).focus(() => {
279 | this.pageIsNotActive = false
280 | if (this.header.notificationsEnabled) {
281 | this.taskList.notifyAboutTask(this.taskList.getActive().id, false)
282 | }
283 | })
284 |
285 | $(window).blur(() => {
286 | this.pageIsNotActive = true
287 | })
288 |
289 | this.configsManager
290 | .refreshConfigs()
291 | .then(() => this.configsManager.setConfig(0))
292 |
293 | this.logger = new WebLogger(getEl('#task-logs'))
294 | this.websocket = new WebSocket(`ws://${location.host}`, {
295 | onLogUpdate: this.handleOnUpdateLog,
296 | onAppListUpdate: this.configsManager.refreshConfigs,
297 | })
298 | })
299 | }
300 | }
301 |
302 | export default new Global()
303 |
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
1 | body,html {
2 | width: 100%;
3 | height: 100%;
4 | font-family: sans-serif;
5 | line-height: 1.15;
6 | -webkit-text-size-adjust: 100%;
7 | -ms-text-size-adjust: 100%;
8 | -ms-overflow-style: scrollbar;
9 | -webkit-tap-highlight-color: transparent;
10 | }
11 | *, ::after, ::before {
12 | box-sizing: border-box;
13 | }
14 | body {
15 | margin: 0;
16 | font-size: 1rem;
17 | font-weight: 400;
18 | line-height: 1.5;
19 | color: #212529;
20 | text-align: left;
21 | background-color: #efefef;
22 | padding: 3% 0%;
23 | overflow: hidden;
24 | }
25 | button{
26 | outline: none;
27 | }
28 | body * {
29 | font-family: 'Roboto', sans-serif;
30 | }
31 |
32 | header {
33 | background: #f1f1f1;
34 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.09);
35 | color: rgb(99, 99, 99);
36 | display: flex;
37 | font-size: 18px;
38 | font-weight: 900;
39 | justify-content: space-between;
40 | position: relative;
41 | width: 100%;
42 | z-index: 5;
43 | }
44 |
45 | .wrapper {
46 | background: #f1f1f1;
47 | box-shadow: 0 0px 57px 1px rgba(0, 0, 0, 0.18), 0 2px 2px rgba(0, 0, 0, 0.1);
48 | height: 100%;
49 | margin-left: auto;
50 | margin-right: auto;
51 | max-width: 876px;
52 | position: relative;
53 | }
54 | .wrapper > section{
55 | float: left;
56 | overflow-y: auto;
57 | }
58 |
59 | #task-list {
60 | height: calc(100% - 40px);
61 | width: 28%;
62 | position: relative;
63 | }
64 |
65 | #task-list::-webkit-scrollbar {
66 | width: 10px;
67 | }
68 | #task-list::-webkit-scrollbar-track {
69 | background: #f1f1f1;
70 | }
71 | #task-list::-webkit-scrollbar-thumb {
72 | background: rgba(0, 0, 0, 0.15);
73 | }
74 | #task-list::-webkit-scrollbar-thumb:hover {
75 | background: rgba(0, 0, 0, 0.2);
76 | }
77 |
78 | #task-logs{
79 | width: 72%;
80 | height: calc(100% - 40px);
81 | overflow: hidden;
82 | display: flex;
83 | flex-direction: column;
84 | }
85 |
86 | .logs-container{
87 | background: #1e1e1e;
88 | word-break: break-all;
89 | text-align: justify;
90 | color: #dedede;
91 | font-size: 13px;
92 | font-family: monospace;
93 | padding: 8px 0 27px 8px;
94 | position: relative;
95 | overflow-x: hidden;
96 | height: 100%;
97 | }
98 | .logs-container::-webkit-scrollbar {
99 | width: 10px;
100 | }
101 | .logs-container::-webkit-scrollbar-thumb {
102 | background: #f1f1f1;
103 | }
104 | .logs-container::-webkit-scrollbar-thumb:hover {
105 | background: rgb(220, 220, 220);
106 | }
107 |
108 | .task {
109 | padding: 0 0 0 9px;
110 | background: rgba(119, 119, 119, 0.05);
111 | color: #7b7b7b;
112 | cursor: pointer;
113 | display: flex;
114 | position: relative;
115 | transition: all 250ms ease;
116 | z-index: 0;
117 | }
118 |
119 | .task > button{
120 | border: none;
121 | background: none;
122 | cursor: pointer;
123 | height: 42px;
124 | font-size: 15px;
125 | width: 32px;
126 | position: absolute;
127 | left: -32px;
128 | transition: left 69ms linear;
129 | }
130 |
131 | .main-buttons {
132 | padding: 0;
133 | display: flex;
134 | overflow: hidden;
135 | }
136 |
137 |
138 | .main-button{
139 | border: 0;
140 | color: #f1f1f1;
141 | font-size: 15px;
142 | cursor: pointer;
143 | padding: 1px 20px;
144 | min-width: 88px;
145 | line-height: 1;
146 | }
147 |
148 |
149 | .main-button:not(.toggle){
150 | margin: 0 0 0 -20px;
151 | overflow:hidden;
152 | position:relative;
153 | -webkit-transform: skew(-20deg);
154 | -moz-transform: skew(-20deg);
155 | -o-transform: skew(-20deg);
156 | transform: skew(-20deg);
157 | }
158 |
159 | .main-button:not(.toggle) > span {
160 | -webkit-transform: skew(20deg);
161 | -moz-transform: skew(20deg);
162 | -o-transform: skew(20deg);
163 | transform: skew(20deg);
164 | position: relative;
165 | display: block;
166 | }
167 |
168 | .run{
169 | background: #2eb97a;
170 | right: 12px;
171 | color: #d5ffec;
172 | }
173 | header > * {
174 | padding: 8px 10px 5px;
175 | }
176 | .stop{background: #b55d5d;color: #ffd5d5;right: -7px;padding-right: 27px;}
177 | .task > .stop-task {
178 | display: none;
179 | color: #a93333;
180 | background: #f38f8f;
181 | }
182 | .task > .run-task {
183 | color: rgb(9, 101, 25);
184 | background: rgb(129, 211, 144);
185 | }
186 | .task-data {
187 | min-height: 32px;
188 | padding: 9px 8px 0 0;
189 | width: auto;
190 | background: transparent;
191 | position: relative;
192 | top: -6px;
193 | display: flex;
194 | }
195 | .task-data > span {
196 | background: #ebebeb;
197 | color: #1d1d1d;
198 | padding: 0 8px;
199 | border-radius: 15px;
200 | display: block;
201 | cursor: default;
202 | transition: all 189ms linear;
203 | }
204 | .task.active {
205 | color: #1d1d1d;
206 | box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.55);
207 | background: rgb(241, 241, 241);
208 | z-index: 2;
209 | }
210 | .task.updated {
211 | animation: highlight cubic-bezier(0.22, 0.61, 0.36, 1) .7s infinite;
212 | }
213 | .run:active {
214 | background: #21d483;
215 | }
216 |
217 | .stop:active {
218 | background: #e65555;
219 | }
220 |
221 | .task i.fa-cog{
222 | display:inline-block;
223 | height: 24px;
224 | position: relative;
225 | left: -5px;
226 | font-size: 24px;
227 | top: 8px;
228 | color: rgba(80, 80, 80, 0);
229 | }
230 |
231 | .task.running .run-task, .task.clicked .run-task, .task.clicked .stop-task, .task.stopping .run-task, .task.stopping .stop-task {
232 | display: none;
233 | }
234 |
235 | .task.running .stop-task {
236 | display: block;
237 | }
238 |
239 | .task.running:before {
240 | background: #75d085;
241 | }
242 |
243 | .task.clicked:before,.task.stopping:before {
244 | background: #bdbdbd;
245 | }
246 | .task.clicked i.fa-cog:before,.task.stopping i.fa-cog:before {
247 | content: "\f110" !important;
248 | }
249 |
250 | .task.running i.fa-cog, .task.clicked i.fa-cog {
251 | -webkit-animation: spin 4s linear infinite;
252 | -moz-animation: spin 4s linear infinite;
253 | animation: spin 4s linear infinite;
254 | }
255 | .task.stopping i.fa-cog {
256 | -webkit-animation: spin 2s linear reverse infinite;
257 | -moz-animation: spin 2s linear reverse infinite;
258 | animation: spin 2s linear reverse infinite;
259 | }
260 | .task.running i.fa-cog{
261 | color: #096519;
262 | }
263 | .task.clicked i.fa-cog{
264 | color: #5c9aad;
265 | }
266 | .task.stopping i.fa-cog{
267 | color: #d45a5a;
268 | }
269 |
270 | .task > span {
271 | user-select: none;
272 | padding: 9px 0;
273 | transition: transform 199ms cubic-bezier(0.18, 0.89, 0.32, 1.28) !important;
274 | text-overflow: ellipsis;
275 | overflow: hidden;
276 | white-space: nowrap;
277 | }
278 |
279 |
280 | .task:before {
281 | content: '';
282 | position: absolute;
283 | left: 0;
284 | top: 0;
285 | width: 32px;
286 | height: 100%;
287 | background: transparent;
288 | z-index: 0;
289 | opacity: .9;
290 | }
291 | .task:hover button {
292 | transition: left 199ms cubic-bezier(0.18, 0.89, 0.32, 1.28) !important;
293 | left: 0;
294 | }
295 |
296 | .task:hover {
297 | box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.35);
298 | background: rgba(241, 241, 241, 0.91);
299 | z-index: 1;
300 | }
301 |
302 | .task:hover:before {
303 | opacity: .3;
304 | }
305 |
306 | .task:not(:hover) > span {
307 | transform: translateX(-27px);
308 | }
309 |
310 | .task.running > span, .task.clicked > span, .task.stopping > span {
311 | transform: translateX(6px);
312 | }
313 | .task.clicked:before,.task.stopping:before {
314 | opacity: 1;
315 | }
316 | .task.clicked:before{
317 | background: #94d6ea !important;
318 | }
319 | .task.stopping:before{
320 | background: #ea9494 !important;
321 | }
322 |
323 | .ansi-black-fg {color: rgba(0,0,0,1) }
324 | .ansi-red-fg {color: rgb(249, 101, 92);}
325 | .ansi-green-fg {color: rgb(63, 216, 104);}
326 | .ansi-yellow-fg {color: rgb(222, 222, 89);}
327 | .ansi-blue-fg {color: rgb(108, 159, 255);}
328 | .ansi-magenta-fg {color: rgb(204, 77, 204);}
329 | .ansi-cyan-fg {color: rgb(90, 218, 218);}
330 | .ansi-white-fg {color: rgba(255,255,255,1) }
331 | .ansi-bright-black-fg {color: #cbcbcb;}
332 | .ansi-bright-red-fg {color: rgba(255,85,85,1) }
333 | .ansi-bright-green-fg {color: rgba(0,255,0,1) }
334 | .ansi-bright-yellow-fg {color: rgba(255,255,85,1) }
335 | .ansi-bright-blue-fg {color: rgb(81, 136, 197);}
336 | .ansi-bright-magenta-fg {color: rgb(236, 97, 236);}
337 | .ansi-bright-cyan-fg {color: rgba(85,255,255,1) }
338 | .ansi-bright-white-fg {color: rgba(255,255,255,1) }
339 | .tussock{color: #c6d2ff;}
340 | .mocha-test {font-size: 16px;}
341 | .bold{font-weight: bold;}
342 |
343 | .logs-container > div > span.ansi-red-fg {
344 | display: block;
345 | }
346 |
347 | .logs-container > div {
348 | padding-bottom: 3px;
349 | font-size: 14px;
350 | }
351 |
352 | span.ended {
353 | font-family: monospace;
354 | color: #85b5ff;
355 | font-weight: 100;
356 | font-size: 14px;
357 | display: block;
358 | padding: 10px 0 16px;
359 | }
360 | span.ended.ok {
361 | color: rgb(53, 212, 118);
362 | }
363 |
364 | .logs-button {
365 | position: absolute;
366 | bottom: 28px;
367 | width: 30px;
368 | height: 30px;
369 | right: 27px;
370 | font-size: 19px;
371 | border: 2px solid;
372 | border-radius: 5px;
373 | background: transparent;
374 | color: #f1f1f1;
375 | opacity: .2;
376 | cursor: pointer;
377 | transition: all 279ms ease;
378 | }
379 |
380 | .logs-button.autoscroll{
381 | padding-top: 2px;
382 | }
383 | .logs-button.scroll-top{
384 | bottom: 134px;
385 | transform: rotate(180deg);
386 | }
387 | .logs-button.clear{
388 | bottom: 72px;
389 | padding-left: 3px;
390 | }
391 |
392 | .logs-button.active, .logs-button:active {
393 | border: 2px solid #f1f1f1;
394 | background: #f1f1f1;
395 | color: rgb(29, 29, 29);
396 | }
397 |
398 | button.logs-button:hover {
399 | opacity: 1;
400 | }
401 | .task.clicked i.fa-cog {
402 | animation-duration: 1s;
403 | }
404 |
405 | button.tab {
406 | /* position: absolute; */
407 | border-top-left-radius: 8px;
408 | user-select: none;
409 | border-top-right-radius: 8px;
410 | border: none;
411 | padding: 1px 0 3px;
412 | z-index: -50;
413 | color: #595959;
414 | background: rgb(239, 239, 239);
415 | cursor: pointer;
416 | box-shadow: 0 0px 0px 0.5px rgb(171, 171, 171);
417 | transition: background 269ms cubic-bezier(0, 0, 0.2, 1);
418 | transform: rotate(-90deg);
419 | margin: 21px -3px;
420 | line-height: 10px;
421 | position: relative;
422 | width: inherit;
423 | }
424 |
425 | button.tab.active, button.tab:active {
426 | z-index: 3;
427 | background: rgb(255, 247, 204);
428 | /* font-weight: 500; */
429 | box-shadow: 0 0px 0px 0.5px rgb(154, 137, 47);
430 | color: #212121;
431 | }
432 |
433 | .envs-log:before{
434 | content:'envs: ';
435 | }
436 |
437 | .tabs {
438 | position: absolute;
439 | left: -46.5px;
440 | top: 0;
441 | z-index: 0;
442 | display: flex;
443 | flex-direction: column;
444 | flex: 1;
445 | height: 100%;
446 | padding-top: 100px;
447 | padding-left: 12px;
448 | width: 58px;
449 | }
450 |
451 | .envs-log {
452 | text-align: left;
453 | }
454 | .envs-log > .env-value {
455 | pointer-events: none;
456 | background: transparent;
457 | border: none;
458 | outline: none;
459 | }
460 |
461 | .envs-log > button.logs-button {
462 | position: relative;
463 | top: 0;
464 | bottom: auto;
465 | float: right;
466 | font-size: 15px;
467 | }
468 |
469 | .logs-button.apply, .logs-button.cancel, .envs-log.active .logs-button {
470 | display: none;
471 | }
472 |
473 | .envs-log.active .logs-button.apply, .envs-log.active .logs-button.cancel {
474 | display: block;
475 | }
476 |
477 | .logs-button.apply {
478 | margin-right: 8px;
479 | color: #95ff7d;
480 | }
481 |
482 | .logs-button.cancel {
483 | color: #ff8c8c;
484 | }
485 |
486 | .envs-log.active > .env-value {
487 | pointer-events: all;
488 | border: 1px solid rgb(235, 235, 235);
489 | border-radius: 6px;
490 | color: #ebebeb;
491 | }
492 |
493 | .scrolling > .task-data {
494 | position: fixed;
495 | left: auto;
496 | right: auto;
497 | top: auto;
498 | width: 546px;
499 | margin-top: -5px;
500 | }
501 | .logo{
502 | position: absolute;
503 | width: 40px;
504 | z-index: -1;
505 | left: -27px;
506 | top: 2px;
507 | padding: 0;
508 | filter: hue-rotate(-26deg);
509 | transform: rotate(-8deg);
510 | }
511 | .title {
512 | color: #1d1d1d;
513 | font-weight: 500;
514 | padding-left: 6px;
515 | background: #f1f1f1;
516 | max-height: 40px;
517 | text-overflow: ellipsis;
518 | overflow: hidden;
519 | z-index: 10;
520 | white-space: nowrap;
521 | max-width: 180px;
522 | cursor: pointer;
523 | position: relative;
524 | }
525 | .title span {
526 | padding-right: 6px;
527 | user-select: none;
528 | }
529 | .title .fas {
530 | position: absolute;
531 | right: 0;
532 | font-size: 14px;
533 | color: #9a9a9a;
534 | line-height: 0.9;
535 | padding: 8px 0 5px;
536 | transition: transform 189ms ease;
537 | }
538 | .project-version {
539 | color: #888888;
540 | margin-left: 5px;
541 | font-size: 12px;
542 | pointer-events: none;
543 | }
544 |
545 |
546 | .task:hover > span {
547 | transform: translateX(6px);
548 | }
549 | .tasks-button{
550 | display: none;
551 | }
552 |
553 | .main-button.toggle {
554 | margin: auto 5px;
555 | border-radius: 25px;
556 | background: #d8d8d8;
557 | color: #5d5d5d;
558 | padding: 2px 15px;
559 | }
560 |
561 |
562 |
563 | .logs-container > * {
564 | font-family: monospace;
565 | }
566 |
567 | .main-button.toggle.hot-keys {
568 | margin-right: 60px;
569 | }
570 |
571 | .main-button.toggle.active {
572 | background: #2eb97a;
573 | color: #dbffef;
574 | }
575 |
576 | .toggle.color .fas {
577 | transition: transform 239ms ease-out;
578 | }
579 |
580 | .scrolling > .task-data > span:not(:hover) {
581 | opacity: .5;
582 | background: rgba(235, 235, 235, 0.58);
583 | color: #1d1d1d;
584 | }
585 |
586 | .logs-wrapper {
587 | position: relative;
588 | flex: 1;
589 | overflow: auto;
590 | }
591 |
592 | .scroll-actions {
593 | position: absolute;
594 | right: 0;
595 | bottom: 0;
596 | z-index: 1;
597 | }
598 |
599 | .configs-list {
600 | position: absolute;
601 | left: -10px;
602 | top: calc(100% - 10px);
603 | width: 240px;
604 | min-height: 40px;
605 | background: #efefef;
606 | z-index: 99999;
607 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.17), 0 2px 10px rgba(0, 0, 0, 0.17);
608 | display: none;
609 | padding: 0;
610 | }
611 |
612 | .config {
613 | font-weight: 400;
614 | cursor: pointer;
615 | color: #212529;
616 | display: flex;
617 | justify-content: space-between;
618 | align-items: center;
619 | padding: 4px 10px 4px;
620 | }
621 |
622 | .config:hover {
623 | background: white;
624 | }
625 |
626 | .config:not(:first-child) {
627 | border-top: 1px solid rgba(0, 0, 0, 0.06);
628 | }
629 |
630 | .config:first-child {
631 | padding-top: 6px;
632 | }
633 |
634 | .config:last-child {
635 | padding-bottom: 6px;
636 | }
637 |
638 | .config .name {
639 | user-select: none;
640 | }
641 |
642 | [config="show"] .configs-list {
643 | display: block;
644 | }
645 | [config="show"] .title .fas {
646 | transform: rotate(180deg);
647 | }
648 |
649 | .close-icon {
650 | color: #b55d5d;
651 | }
652 |
653 | @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
654 | @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
655 | @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
656 | @keyframes highlight{
657 | 50%{
658 | background: rgb(255, 247, 204);
659 | }
660 | }
661 |
--------------------------------------------------------------------------------