Skip to content

Commit

Permalink
feat(images): add image view (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
OmriBarZik authored Jun 19, 2021
1 parent 8e44a85 commit 834236e
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<br>
<img width="200" src="https://user-images.githubusercontent.com/316371/28937414-67ee5ffa-7893-11e7-95f9-5059cacf9170.png">
<br>
Immersive terminal interface for managing docker containers and services
Immersive terminal interface for managing docker containers, services and images
</p>


Expand Down
107 changes: 107 additions & 0 deletions hooks/images.hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict'

const EventEmitter = require('events')
const baseWidget = require('../src/baseWidget')
const clipboardy = require('clipboardy')

class hook extends baseWidget(EventEmitter) {
init () {
if (!this.widgetsRepo.has('toolbar')) {
return null
}

this.notifyOnImageUpdate()

const toolbar = this.widgetsRepo.get('toolbar')
toolbar.on('key', (keyString) => {
// on refresh keypress, update all containers and images information

if (keyString === 'r') {
this.removeImage()
}

if (keyString === 'c') {
this.copyImageIdToClipboard()
}
})
}

copyImageIdToClipboard () {
if (this.widgetsRepo && this.widgetsRepo.has('imageList')) {
const imageId = this.widgetsRepo.get('imageList').getSelectedImage()
if (imageId) {
clipboardy.writeSync(imageId)

const actionStatus = this.widgetsRepo.get('actionStatus')
const message = `Image Id ${imageId} was copied to the clipboard`

actionStatus.emit('message', {
message: message
})
}
}
}

notifyOnImageUpdate () {
setInterval(() => {
if (this.widgetsRepo && this.widgetsRepo.has('imageList')) {
// Update on Docker Info
this.utilsRepo.get('docker').systemDf((data) => {
if (!data.Images) {
return
}

const UseImages = []
const UnuseImages = []

data.Images.forEach(image => {
if (image.Containers > 0) {
UseImages.push(image)
} else {
UnuseImages.push(image)
}
})

data.UseImages = UseImages
data.UnuseImages = UnuseImages

this.emit('imagesUtilization', data)
this.emit('imageSize', data)
})
}
}, 1000)
}

removeImage () {
if (this.widgetsRepo && this.widgetsRepo.has('imageList')) {
const imageId = this.widgetsRepo.get('imageList').getSelectedImage()
if (imageId && imageId !== 0 && imageId !== false) {
const actionStatus = this.widgetsRepo.get('actionStatus')

const title = 'Removing image'
let message = 'Removing image...'

actionStatus.emit('message', {
title: title,
message: message
})

this.utilsRepo.get('docker').removeImage(imageId, (err, data) => {
if (err) {
message = err.json.message
} else {
message = 'Removed image successfully'
this.widgetsRepo.get('imageList').refreshList()
}

actionStatus.emit('message', {
title: title,
message: message
})
})
}
}
}
}

module.exports = hook
3 changes: 2 additions & 1 deletion lib/modes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

module.exports = {
container: 'containers',
service: 'services'
service: 'services',
image: 'images'
}
15 changes: 15 additions & 0 deletions src/dockerUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ class Util {
})
}

systemDf (cb) {
this.dockerCon.df((err, data) => {
if (err) {
return cb(err, {})
}

return cb(data)
})
}

listContainers (cb) {
this.dockerCon.listContainers({
'all': true,
Expand Down Expand Up @@ -151,6 +161,11 @@ class Util {
return service.inspect(cb)
}

getImage (imageId, cb) {
const image = this.dockerCon.getImage(imageId)
image.inspect(cb)
}

restartContainer (containerId, cb) {
const container = this.dockerCon.getContainer(containerId)
container.restart(cb)
Expand Down
17 changes: 16 additions & 1 deletion src/screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ const SERVICES_GRID_LAYOUT = {
'toolbar': [11, 0, 1, 12]
}

const IMAGES_GRID_LAYOUT = {
'imageInfo': [2, 2, 8, 8],
'imageList': [0, 0, 6, 10],
'searchInput': [11, 0, 1, 12],
'actionStatus': [6, 0, 1, 10],
'help': [4, 4, 4, 4],
'toolbar': [11, 0, 1, 12],
'imageUtilization': [0, 10, 2, 2]
}

const GRID_LAYOUT = {}
GRID_LAYOUT[MODES.container] = CONTAINERS_GRID_LAYOUT
GRID_LAYOUT[MODES.service] = SERVICES_GRID_LAYOUT
GRID_LAYOUT[MODES.image] = IMAGES_GRID_LAYOUT

class screen {
constructor (utils = new Map()) {
this.mode = MODES.container
Expand Down Expand Up @@ -105,7 +120,7 @@ class screen {
}

initWidgets () {
const layout = this.mode === MODES.container ? CONTAINERS_GRID_LAYOUT : SERVICES_GRID_LAYOUT
const layout = GRID_LAYOUT[this.mode]
for (let [widgetName, WidgetObject] of this.assets.get('widgets').entries()) {
if (layout[widgetName]) {
let widget = new WidgetObject({
Expand Down
10 changes: 7 additions & 3 deletions src/widgetsTemplates/help.widget.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ class myWidget extends baseWidget() {
▸ h: Show/hide this window
▸ <space>: Refresh the current view
▸ /: Search the containers list view
▸ i: Display information dialog about the selected container or service
▸ /: Search the current list view
▸ i: Display information dialog about the selected container, service, or images
▸ ⏎: Show the logs of the current container or service
▸ c: Copy id to the clipboard
▸ v: Toggle between Containers and Services view
▸ v: Toggle between Containers, Services, and images view
▸ q: Quit dockly
The following commands are only available in Container view:
Expand All @@ -118,6 +118,10 @@ class myWidget extends baseWidget() {
▸ s: Stop the selected container
▸ m: Show a menu with additional actions
The following commands are only available in Image view:
▸ r: Remove the selected image
Thanks for using dockly!
`
}
Expand Down
19 changes: 19 additions & 0 deletions widgets/images/imageInfo.widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

const InfoWidget = require('../../src/widgetsTemplates/info.widget.template')

class myWidget extends InfoWidget {
getLabel () {
return 'Image Info'
}

getSelectedItemId () {
return this.widgetsRepo.get('imageList').getSelectedImage()
}

getItemById (itemId, cb) {
return this.utilsRepo.get('docker').getImage(itemId, cb)
}
}

module.exports = myWidget
116 changes: 116 additions & 0 deletions widgets/images/imageList.widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict'

const ListWidget = require('../../src/widgetsTemplates/list.widget.template')

class myWidget extends ListWidget {
constructor ({ blessed = {}, contrib = {}, screen = {}, grid = {} }) {
super({ blessed, contrib, screen, grid })
this.imagesListData = []
}

getLabel () {
return 'Images'
}

getListItems (cb) {
this.utilsRepo.get('docker').listImages(cb)
}

filterList (data) {
let imageTitleList = this.imagesListData[0]
let imageList = this.imagesListData.slice(1)
let filteredimages = []

if (data) {
filteredimages = imageList.filter((container, index, containerItems) => {
const imageName = container[1]
const imageTag = container[2]

if ((imageName.indexOf(data) !== -1) || (imageTag.indexOf(data) !== -1)) {
return true
}
})
}

if (filteredimages.length > 0) {
filteredimages.unshift(imageTitleList)
this.update(filteredimages)
} else {
this.update(this.imagesListData)
}
}

formatList (images) {
const imageList = []

if (images) {
images.forEach((image) => {
const getTag = (tag, part) => tag ? tag[0].split(':')[part] : 'none'

imageList.push([
image.Id.substring(7, 12),
image.RepoDigests ? image.RepoDigests[0].split('@')[0] : getTag(image[2], 0),
getTag(image.RepoTags, 1),
this.timeDifference(image.Created),
this.formatBytes(image.Size)
])
})
}

imageList.unshift(['Id', 'Name', 'Tag', 'Created', 'Size'])

this.imagesListData = imageList

return imageList
}

/**
* Format raw bytes into human readable size.
*
* @param {number} bytes - number of bytes.
* @returns {string} human readable size.
*/
formatBytes (bytes, decimals) {
if (bytes === 0) return '0 Bytes'
let k = 1000

let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

let i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}

/**
* Convert number of seceond to
* @param {number} creationDate Images creation date in unix time.
* @returns
*/
timeDifference (creationDate) {
const msPerMinute = 60 * 1000
const msPerHour = msPerMinute * 60
const msPerDay = msPerHour * 24
const msPerMonth = msPerDay * 30
const msPerYear = msPerDay * 365

const elapsed = Date.now() - (creationDate * 1000)

const cleanReturn = (number, format) => `${Math.round(number)} ${format}${Math.round(number) === 1 ? '' : 's'} ago`

if (elapsed < msPerMinute) { return cleanReturn(elapsed / 1000, 'second') }
if (elapsed < msPerHour) { return cleanReturn(elapsed / msPerMinute, 'minute') }
if (elapsed < msPerDay) { return cleanReturn(elapsed / msPerHour, 'hour') }
if (elapsed < msPerMonth) { return cleanReturn(elapsed / msPerDay, 'day') }
if (elapsed < msPerYear) { return cleanReturn(elapsed / msPerMonth, 'month') }
return cleanReturn(elapsed / msPerYear, 'year')
}

/**
* returns a selected container from the containers listbox
* @return {string} container id
*/
getSelectedImage () {
return this.widget.getItem(this.widget.selected).getContent().toString().trim().split(' ').shift()
}
}

module.exports = myWidget
Loading

0 comments on commit 834236e

Please sign in to comment.