Skip to content
This repository was archived by the owner on Sep 5, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ Setting the `NODE_TLS_REJECT_UNAUTHORIZED` environment variable to `0` will inst

This should only impact on-premise deployments of Looker. Do not set this environment variable if Looker hosts your instance.

##### Using Dashboard configs

Dashboard description can be used to store json config:

```
{
"image_width": 1024,
"image_height": 512,
"description": "this is a dashboard to do magic",
"tableAsImage": true
}
```

If the description is not JSON it will be used in the help text

##### Connecting the bot to multiple Looker instances

If you would like the bot to connect to multiple instances of Looker, then you can configure the bot with the `LOOKERS` environment variable. This variable should be JSON array of JSON objects, each representing a Looker instance and its authentication information.
Expand Down
2 changes: 1 addition & 1 deletion src/commands/custom_command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class CustomCommand extends Command {
for (const filter of dashboardFilters) {
filters[filter.name] = query
}
const runner = new DashboardQueryRunner(context, matchedCommand.dashboard, filters)
const runner = new DashboardQueryRunner(context, matchedCommand.dashboard, matchedCommand.config, filters)
runner.start()

return true
Expand Down
29 changes: 26 additions & 3 deletions src/looker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export interface ICustomCommand {
category: string
helptext?: string
hidden: boolean
config?: IQueryConfig
}

export interface IQueryConfig {
tableAsImage?: boolean | false
image_height?: number
image_width?: number
description: string | ""
}

interface ILookerOptions {
Expand Down Expand Up @@ -98,15 +106,30 @@ export class Looker {
name: dashboard.title.toLowerCase().trim(),
}

command.hidden = (category.toLowerCase().indexOf("[hidden]") !== -1) || (command.name.indexOf("[hidden]") !== -1)
const helpTextItems = []

command.helptext = ""
command.hidden = (category.toLowerCase().indexOf("[hidden]") !== -1) || (command.name.indexOf("[hidden]") !== -1)

const dashboardFilters = dashboard.dashboard_filters || dashboard.filters
if (dashboardFilters && dashboardFilters.length > 0) {
command.helptext = `<${dashboardFilters[0].title.toLowerCase()}>`
helpTextItems.push(`<${dashboardFilters[0].title.toLowerCase()}>`)
}

if (dashboard.description && dashboard.description.trim().startsWith("{")) {
try {
command.config = JSON.parse(dashboard.description)
if (command.config) {
command.description = command.config.description
}
} catch (e) {
helpTextItems.push("WARNING: dashboard description is not valid json or starts with {")
// gives cleaner output, if dashboard has an error
command.description = ""
}
}

command.helptext = helpTextItems.join(" — ")

Looker.customCommands[command.name] = command
},

Expand Down
18 changes: 17 additions & 1 deletion src/looker_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as crypto from "crypto"
import * as request from "request"
import * as _ from "underscore"
import config from "./config"
import { IQueryConfig } from "./looker"
import { ReplyContext } from "./reply_context"

export interface ILookerRequestConfig {
Expand All @@ -13,6 +14,7 @@ export interface ILookerRequestConfig {

export interface ILookerRequestOptions {
encoding?: string | null
params?: IQueryConfig | null
}

export class LookerAPIClient {
Expand Down Expand Up @@ -59,6 +61,19 @@ export class LookerAPIClient {
url: `${this.options.baseUrl}/${requestConfig.path}`,
}

if (requestConfig.params) {

newConfig.formData = {}

if (requestConfig.params.image_height) {
newConfig.formData.image_width = requestConfig.params.image_height
}

if (requestConfig.params.image_width) {
newConfig.formData.image_width = requestConfig.params.image_width
}
}

if (typeof requestConfig.encoding !== "undefined") {
newConfig.encoding = requestConfig.encoding
}
Expand Down Expand Up @@ -117,8 +132,9 @@ export class LookerAPIClient {
public getBinaryAsync(
path: string,
replyContext?: ReplyContext,
options?: ILookerRequestOptions,
): Promise<Buffer> {
return this.getAsync(path, replyContext, {encoding: null})
return this.getAsync(path, replyContext, options)
}

public post(path: string, body: any, successCallback?: any, errorCallback?: any, replyContext?: ReplyContext) {
Expand Down
5 changes: 4 additions & 1 deletion src/repliers/dashboard_query_runner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IQueryConfig } from "../looker"
import { IDashboard, IQuery } from "../looker_api_types"
import { ReplyContext } from "../reply_context"
import { QueryRunner } from "./query_runner"
Expand All @@ -7,9 +8,10 @@ export class DashboardQueryRunner extends QueryRunner {
constructor(
replyContext: ReplyContext,
private dashboard: IDashboard,
private config?: IQueryConfig,
private filters: {[key: string]: string} = {},
) {
super(replyContext)
super(replyContext, undefined, config)
this.dashboard = dashboard
this.filters = filters
}
Expand Down Expand Up @@ -66,6 +68,7 @@ export class DashboardQueryRunner extends QueryRunner {
{},
this.replyContext,
)

this.runQuery(query)
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/repliers/query_runner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as _ from "underscore"
import { IQueryConfig } from "../looker"
import { IQuery, IQueryResponse } from "../looker_api_types"
import { ReplyContext } from "../reply_context"
import { SlackUtils } from "../slack_utils"
import blobStores from "../stores/index"
import { FancyReplier } from "./fancy_replier"
import { SlackTableFormatter } from "./slack_table_formatter"
Expand All @@ -10,11 +10,13 @@ export class QueryRunner extends FancyReplier {

protected querySlug?: string
protected queryId?: number
protected queryConfig: IQueryConfig

constructor(replyContext: ReplyContext, queryParam: {slug?: string, id?: number} = {}) {
constructor(replyContext: ReplyContext, queryParam: {slug?: string, id?: number} = {}, queryConfig: IQueryConfig = {} as IQueryConfig) {
super(replyContext)
this.querySlug = queryParam.slug
this.queryId = queryParam.id
this.queryConfig = queryConfig
}

protected showShareUrl() { return false }
Expand Down Expand Up @@ -100,7 +102,7 @@ export class QueryRunner extends FancyReplier {
protected async runQuery(query: IQuery) {
const visType: string = query.vis_config && query.vis_config.type ? query.vis_config.type : "table"

if (visType === "table" || visType === "looker_single_record" || visType === "single_value") {
if ((visType === "table" && !this.queryConfig.tableAsImage) || visType === "looker_single_record" || visType === "single_value") {
try {
const result = await this.replyContext.looker.client.getAsync(
`queries/${query.id}/run/unified`,
Expand All @@ -115,6 +117,7 @@ export class QueryRunner extends FancyReplier {
const imageData = await this.replyContext.looker.client.getBinaryAsync(
`queries/${query.id}/run/png`,
this.replyContext,
{ encoding: null, params: this.queryConfig },
)
this.postImage(query, imageData)
} catch (e) {
Expand Down