Skip to content

Instantly share code, notes, and snippets.

@malesfth
Last active October 31, 2023 15:20
Show Gist options
  • Save malesfth/38885be4617f61913af74a695be82b07 to your computer and use it in GitHub Desktop.
Save malesfth/38885be4617f61913af74a695be82b07 to your computer and use it in GitHub Desktop.
iOS Scriptable Widget for Portainer (Docker)
// Parameters:
// {"url":"https://portainer:9000","user":"username","pass":"secret"}
// Optional key in parameters: "endpoint": 1|2|...
// Optional key in parameters: "theme": system|light|dark|docker
// Optional key in parameters: "name": "Portainer|Docker"
let portainerUser = "" // set the username here for debug
let portainerPass = "" // set the password here for debug
let portainerURL = "" //set the URL here for debug
let portainerEndpoint = "1" //set the endpoint id for debug
let wTheme = "system" // set the theme for debug
let wName = "Portainer"
if (config.runsInWidget) {
const widgetParams = (args.widgetParameter != null ? JSON.parse(args.widgetParameter) : null)
if (widgetParams==null) {
throw new Error("Please long press the widget and add the parameters.")
} else if (!widgetParams.hasOwnProperty("url") && !widgetParams.hasOwnProperty("username") && !widgetParams.hasOwnProperty("pass")) {
throw new Error("Wrong parameters.")
}
portainerURL = widgetParams.url
portainerUser = widgetParams.user
portainerPass = widgetParams.pass
if (widgetParams.hasOwnProperty("endpoint")) {
portainerEndpoint = widgetParams.endpoint
}
if (widgetParams.hasOwnProperty("theme")) {
wTheme = widgetParams.theme
}
if (widgetParams.hasOwnProperty("name")) {
if (widgetParams.name=="Docker") {
wName = widgetParams.name
}
}
}
let wBackground = new LinearGradient()
let wColor = new Color("#ffffff")
setTheme()
let portainerStats = await getStats()
let wSize = config.widgetFamily || "small" //set size of widget for debug
let widget = await createWidget() || null
if (!config.runsInWidget) {
if (wSize=="large") { await widget.presentLarge() }
else if (wSize=="medium") { await widget.presentMedium() }
else { await widget.presentSmall() }
}
Script.setWidget(widget)
Script.complete()
async function createWidget() {
let content = null
let w = new ListWidget()
w.backgroundGradient = wBackground
w.addSpacer()
if (portainerStats==null) {
w.setPadding(5, 15, 0, 0)
content = w.addText(wName)
content.font = Font.blackSystemFont(16)
content.textColor = wColor
w.addSpacer(3)
content = w.addText("No connection")
content.font = Font.thinSystemFont(14)
content.textColor = wColor
w.addSpacer()
return w
}
w.url = portainerURL
let iRunning = 0
let iNotRunning = 0
let iContainers = portainerStats.length
for(var i = 0; i < portainerStats.length; i++) {
if (portainerStats[i].State=="running") {
iRunning = iRunning + 1
} else {
iNotRunning = iNotRunning + 1
}
}
if (wSize=="large" || wSize=="medium") {
let maxRows = (wSize=="large" ? 8 : 4)
let arrShow = []
if (iContainers>maxRows) {
for (var i = 0; i < maxRows; i++) {
arrShow = getRandom(arrShow)
}
} else {
for (var i = 0; i < iContainers; i++) {
arrShow.push(i)
}
}
let topStack = w.addStack()
topStack.setPadding(15,0,2,0)
let topStr = topStack.addText(wName)
topStr.font = Font.blackSystemFont(16)
topStr.textColor = wColor
topStack.addSpacer()
topStr = topStack.addText(iRunning +" / "+ iContainers + " container running")
topStr.font = Font.lightSystemFont(9)
topStr.textColor = wColor
w.addSpacer(5)
let layoutStack = w.addStack()
layoutStack.layoutVertically()
for(var i = 0; i < arrShow.length; i++) {
let cntnr = portainerStats[arrShow[i]]
let state = (cntnr.State=="running" ? true : false)
let icn = SFSymbol.named((state ? "circle.fill" : "circle"))
let row = layoutStack.addStack()
row.setPadding(5,0,0,0)
content = row.addImage(icn.image)
content.tintColor = (state ? Color.green() : Color.red())
content.imageSize = new Size(16,16)
row.addSpacer(20)
content = row.addText((cntnr.Names[0]).replace("/",""))
content.font = Font.boldMonospacedSystemFont(14)
content.textColor = wColor
row.addSpacer()
let txt = (cntnr.Status).replace(" (healthy)", "")
content = row.addText(txt)
content.font = Font.thinSystemFont(16)
content.textColor = wColor
}
layoutStack.addSpacer()
} else {
let rowStack = w.addStack()
rowStack.layoutVertically()
rowStack.topAlignContent()
let innerStack = rowStack.addStack()
let txt = innerStack.addText(wName)
txt.font = Font.blackSystemFont(16)
txt.textColor = wColor
innerStack.addSpacer()
innerStack = rowStack.addStack()
txt = innerStack.addText("—")
txt.font = Font.thinSystemFont(16)
txt.textColor = wColor
innerStack.addSpacer(3)
innerStack = rowStack.addStack()
txt = innerStack.addText(iRunning +" / "+ iContainers)
txt.font = Font.blackSystemFont(16)
txt.textColor = wColor
innerStack.addSpacer(5)
txt = innerStack.addText("running")
txt.font = Font.italicSystemFont(16)
txt.textColor = wColor
innerStack.addSpacer(3)
}
w.addSpacer()
return w
}
function setTheme() {
if (wTheme=="system") {
if (Device.isUsingDarkAppearance()) {
wTheme = "dark"
} else {
wTheme = "light"
}
}
wBackground.locations = [0, 1]
if (wTheme=="docker") {
wBackground.colors = [
new Color("#0db7ede6"),
new Color("#0db7ed")
]
wColor = new Color("#ffffff")
} else if (wTheme=="dark") {
wBackground.colors = [
new Color("#384d54"),
new Color("#384d54")
]
wColor = new Color("#ffffff")
} else {
wBackground.colors = [
new Color("#ffffffe6"),
new Color("#ffffffe6")
]
wColor = new Color("#000000")
}
}
function getRandom(arr) {
let count = portainerStats.lenght
let rand = Math.floor(Math.random()*portainerStats.length)
if (arr.indexOf(rand)==-1) {
arr.push(rand)
} else {
return getRandom(arr)
}
return arr
}
async function getStats() {
try {
let url = portainerURL
let req = new Request(url+"/api/auth")
req.allowInsecureRequest = true
req.method = "POST"
req.headers = {"Accept":"application/json"}
req.body = JSON.stringify({"Username":portainerUser,"Password":portainerPass})
let json = await req.loadJSON()
let bearer = json.jwt || null
if (bearer!=null){
req = new Request(url+"/api/endpoints/"+portainerEndpoint+"/docker/containers/json")
req.allowInsecureRequest = true
req.method = "GET"
req.headers = {"Authorization":"Bearer "+bearer}
json = await req.loadJSON()
return json
} else {
throw new Error("Error getting Bearer Token.")
}
} catch {
return null
}
}
@malesfth
Copy link
Author

Because of the insecure connection. It is fixed now within the new version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment