From c1d200794942d6781b14235562b8dc67811ab30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BE=C3=B3r=20Magn=C3=BAsson?= Date: Tue, 12 Dec 2023 12:20:54 +0100 Subject: [PATCH] improvement(dashboard): better warning logs (#5538) This commit adds a message about the dashboard to all Garden commands. Previously we only showed it for the 'dev' command. The message can be disabled. It also consistently logs a warning if a user is logged in across all cloud distros. --- core/src/cli/cli.ts | 34 ++++++++------ core/src/cli/helpers.ts | 12 ++++- core/src/commands/serve.ts | 14 +++--- core/src/garden.ts | 26 ++++++----- core/src/server/server.ts | 2 +- core/src/util/cloud.ts | 6 +-- core/test/unit/src/commands/login.ts | 22 ++++----- core/test/unit/src/commands/logout.ts | 12 ++--- core/test/unit/src/commands/tools.ts | 2 +- core/test/unit/src/garden.ts | 66 ++++++++++++++++++++++++++- core/test/unit/src/util/cloud.ts | 8 ++-- 11 files changed, 142 insertions(+), 62 deletions(-) diff --git a/core/src/cli/cli.ts b/core/src/cli/cli.ts index 65d7519f0a..18c992415b 100644 --- a/core/src/cli/cli.ts +++ b/core/src/cli/cli.ts @@ -35,11 +35,18 @@ import { checkRequirements, renderCommandErrors, cliStyles, + getDashboardInfoMsg, } from "./helpers.js" import type { ParameterObject, GlobalOptions, ParameterValues } from "./params.js" import { globalOptions, OUTPUT_RENDERERS } from "./params.js" import type { ProjectConfig } from "../config/project.js" -import { ERROR_LOG_FILENAME, DEFAULT_GARDEN_DIR_NAME, LOGS_DIR_NAME, gardenEnv } from "../constants.js" +import { + ERROR_LOG_FILENAME, + DEFAULT_GARDEN_DIR_NAME, + LOGS_DIR_NAME, + gardenEnv, + DEFAULT_GARDEN_CLOUD_DOMAIN, +} from "../constants.js" import { generateBasicDebugInfoReport } from "../commands/get/get-debug-info.js" import type { AnalyticsHandler } from "../analytics/analytics.js" import type { GardenPluginReference } from "../plugin/plugin.js" @@ -244,13 +251,10 @@ ${renderCommands(commands)} !command.noProject && command.getFullName() !== "dev" && command.getFullName() !== "serve" ? log.createLog({ name: "garden", showDuration: true }) : null + gardenInitLog?.info("Initializing...") // Init Cloud API (if applicable) let cloudApi: CloudApi | undefined - - if (gardenInitLog) { - gardenInitLog.info("Initializing...") - } if (!command.noProject) { const config = await this.getProjectConfig(log, workingDir) const cloudDomain = getGardenCloudDomain(config?.domain) @@ -319,6 +323,16 @@ ${renderCommands(commands)} garden = await makeDummyGarden(workingDir, contextOpts) } else { garden = await wrapActiveSpan("initializeGarden", () => this.getGarden(workingDir, contextOpts)) + const isLoggedIn = !!cloudApi + const isCommunityEdition = garden.cloudDomain === DEFAULT_GARDEN_CLOUD_DOMAIN + + if (!isLoggedIn && isCommunityEdition) { + await garden.emitWarning({ + key: "web-app", + log, + message: "\n" + getDashboardInfoMsg(), + }) + } if (!gardenEnv.GARDEN_DISABLE_VERSION_CHECK) { await garden.emitWarning({ @@ -333,16 +347,6 @@ ${renderCommands(commands)} gardenLog.info(`Running in environment ${styles.highlight(`${garden.environmentName}.${garden.namespace}`)}`) - if (!cloudApi && garden.projectId) { - log.info("") - log.warn( - `Warning: You are not logged in into Garden Cloud. Please log in via the ${styles.command( - "garden login" - )} command.` - ) - log.info("") - } - if (processRecord) { // Update the db record for the process await globalConfigStore.update("activeProcesses", String(processRecord.pid), { diff --git a/core/src/cli/helpers.ts b/core/src/cli/helpers.ts index 4080ebe455..aee3929457 100644 --- a/core/src/cli/helpers.ts +++ b/core/src/cli/helpers.ts @@ -23,7 +23,7 @@ import { globalDisplayOptions } from "./params.js" import { GardenError, ParameterError, RuntimeError, toGardenError } from "../exceptions.js" import { getPackageVersion, removeSlice } from "../util/util.js" import type { Log } from "../logger/log-entry.js" -import { STATIC_DIR, gardenEnv, ERROR_LOG_FILENAME } from "../constants.js" +import { STATIC_DIR, gardenEnv, ERROR_LOG_FILENAME, DOCS_BASE_URL } from "../constants.js" import { printWarningMessage } from "../logger/util.js" import type { GlobalConfigStore } from "../config-store/global.js" import { got } from "../util/http.js" @@ -563,3 +563,13 @@ export function renderCommandErrors(logger: Logger, errors: Error[], log?: Log) errorLog.info(`\nSee .garden/${ERROR_LOG_FILENAME} for detailed error message`) } } + +export function getDashboardInfoMsg() { + return styles.success(deline` + šŸŒæ Log in with ${styles.command( + "garden login" + )} to explore logs, past commands, and your dependency graph in the Garden dashboard. + + Learn more at: ${styles.underline(`${DOCS_BASE_URL}/using-garden/dashboard`)}\n + `) +} diff --git a/core/src/commands/serve.ts b/core/src/commands/serve.ts index 2a15d6ad4c..e5ace8ad62 100644 --- a/core/src/commands/serve.ts +++ b/core/src/commands/serve.ts @@ -24,6 +24,8 @@ import type { Garden } from "../garden.js" import type { GardenPluginReference } from "../plugin/plugin.js" import { CommandError, ParameterError, isEAddrInUseException, isErrnoException } from "../exceptions.js" import { styles } from "../logger/styles.js" +import { getDashboardInfoMsg } from "../cli/helpers.js" +import { DEFAULT_GARDEN_CLOUD_DOMAIN } from "../constants.js" export const defaultServerPort = 9777 @@ -156,16 +158,14 @@ export class ServeCommand< try { const cloudApi = await manager.getCloudApi({ log, cloudDomain, globalConfigStore: garden.globalConfigStore }) + const isLoggedIn = !!cloudApi + const isCommunityEdition = cloudDomain === DEFAULT_GARDEN_CLOUD_DOMAIN - if (!cloudApi) { + if (!isLoggedIn && isCommunityEdition) { await garden.emitWarning({ key: "web-app", log, - message: styles.success( - `šŸŒæ Explore logs, past commands, and your dependency graph in the Garden dashboard. Log in with ${styles.command( - "garden login" - )}.` - ), + message: getDashboardInfoMsg(), }) } @@ -192,7 +192,7 @@ export class ServeCommand< if (session?.shortId) { const distroName = getCloudDistributionName(cloudDomain) const livePageUrl = cloudApi.getLivePageUrl({ shortId: session.shortId }).toString() - const msg = dedent`${printEmoji("šŸŒø", log)}Connected to ${distroName} ${printEmoji("šŸŒø", log)} + const msg = dedent`\n${printEmoji("šŸŒø", log)}Connected to ${distroName} ${printEmoji("šŸŒø", log)} Follow the link below to stream logs, run commands, and more from the Garden dashboard ${printEmoji( "šŸ‘‡", log diff --git a/core/src/garden.ts b/core/src/garden.ts index 53121c29fb..f0d2059bad 100644 --- a/core/src/garden.ts +++ b/core/src/garden.ts @@ -77,6 +77,7 @@ import { SUPPORTED_ARCHITECTURES, GardenApiVersion, DOCS_BASE_URL, + DEFAULT_GARDEN_CLOUD_DOMAIN, } from "./constants.js" import type { Log } from "./logger/log-entry.js" import { EventBus } from "./events/events.js" @@ -186,7 +187,7 @@ export interface GardenOpts { */ gardenInitLog?: Log monitors?: MonitorManager - noEnterprise?: boolean + skipCloudConnect?: boolean persistent?: boolean plugins?: RegisterPluginParam[] sessionId?: string @@ -562,7 +563,7 @@ export class Garden { if (!existing || !existing.hidden) { this.emittedWarnings.add(key) - log.warn(message + `\nā†’ Run ${styles.underline(`garden util hide-warning ${key}`)} to disable this warning.`) + log.warn(message + `\nā†’ Run ${styles.underline(`garden util hide-warning ${key}`)} to disable this message.`) } }) } @@ -1868,18 +1869,17 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar const projectApiVersion = config.apiVersion const sessionId = opts.sessionId || uuidv4() const cloudApi = opts.cloudApi || null + const isCommunityEdition = !config.domain + const distroName = getCloudDistributionName(cloudApi?.domain || DEFAULT_GARDEN_CLOUD_DOMAIN) + const debugLevelCommands = ["dev", "serve", "exit", "quit"] + const cloudLogLevel = debugLevelCommands.includes(opts.commandInfo.name) ? LogLevel.debug : undefined + const cloudLog = log.createLog({ name: getCloudLogSectionName(distroName), fixLevel: cloudLogLevel }) let secrets: StringMap = {} let cloudProject: CloudProject | null = null // If true, then user is logged in and we fetch the remote project and secrets (if applicable) - if (!opts.noEnterprise && cloudApi) { - const distroName = getCloudDistributionName(cloudApi.domain) - const isCommunityEdition = !config.domain - const cloudLogLevel = - opts.commandInfo.name === "dev" || opts.commandInfo.name === "serve" ? LogLevel.debug : undefined - const cloudLog = log.createLog({ name: getCloudLogSectionName(distroName), fixLevel: cloudLogLevel }) - - cloudLog.info(`Connecting to ${distroName}...`) + if (!opts.skipCloudConnect && cloudApi) { + cloudLog.info(`Connecting project...`) cloudProject = await getCloudProject({ cloudApi, @@ -1909,6 +1909,10 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar } cloudLog.success("Ready") + } else if (!opts.skipCloudConnect) { + cloudLog.warn( + `You are not logged in. To use ${distroName}, log in with the ${styles.command("garden login")} command.` + ) } const loggedIn = !!cloudApi @@ -2176,7 +2180,7 @@ export async function makeDummyGarden(root: string, gardenOpts: GardenOpts) { } gardenOpts.config = config - return DummyGarden.factory(root, { noEnterprise: true, ...gardenOpts }) + return DummyGarden.factory(root, { skipCloudConnect: true, ...gardenOpts }) } export interface ConfigDump { diff --git a/core/src/server/server.ts b/core/src/server/server.ts index 8529bc1277..757fcf69d0 100644 --- a/core/src/server/server.ts +++ b/core/src/server/server.ts @@ -238,7 +238,7 @@ export class GardenServer extends EventEmitter { } } while (!serverStarted) } - this.log.info(`Garden server has successfully started at port ${styles.highlight(this.port.toString())}.\n`) + this.log.info(`Garden server has successfully started at port ${styles.highlight(this.port.toString())}\n`) const processRecord = await this.globalConfigStore.get("activeProcesses", String(process.pid)) diff --git a/core/src/util/cloud.ts b/core/src/util/cloud.ts index c4ae3b067b..b5d023cbf4 100644 --- a/core/src/util/cloud.ts +++ b/core/src/util/cloud.ts @@ -7,7 +7,7 @@ */ import { DEFAULT_GARDEN_CLOUD_DOMAIN } from "../constants.js" -export type CloudDistroName = "Garden Dashboard" | "Garden Enterprise" | "Garden Cloud" +export type CloudDistroName = "the Garden dashboard" | "Garden Enterprise" | "Garden Cloud" /** * Returns "Garden Cloud" if domain matches https://.app.garden, @@ -17,7 +17,7 @@ export type CloudDistroName = "Garden Dashboard" | "Garden Enterprise" | "Garden */ export function getCloudDistributionName(domain: string): CloudDistroName { if (domain === DEFAULT_GARDEN_CLOUD_DOMAIN) { - return "Garden Dashboard" + return "the Garden dashboard" } // TODO: consider using URL object instead. @@ -31,7 +31,7 @@ export function getCloudDistributionName(domain: string): CloudDistroName { export type CloudLogSectionName = "garden-dashboard" | "garden-cloud" | "garden-enterprise" export function getCloudLogSectionName(distroName: CloudDistroName): CloudLogSectionName { - if (distroName === "Garden Dashboard") { + if (distroName === "the Garden dashboard") { return "garden-dashboard" } else if (distroName === "Garden Cloud") { return "garden-cloud" diff --git a/core/test/unit/src/commands/login.ts b/core/test/unit/src/commands/login.ts index 3021a09b9e..129b306d9e 100644 --- a/core/test/unit/src/commands/login.ts +++ b/core/test/unit/src/commands/login.ts @@ -62,7 +62,7 @@ describe("LoginCommand", () => { } const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -88,7 +88,7 @@ describe("LoginCommand", () => { } const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -115,7 +115,7 @@ describe("LoginCommand", () => { const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -146,7 +146,7 @@ describe("LoginCommand", () => { // NOTE: if we don't use makeDummyGarden it would try to fully resolve the // secrets which are not available unless we mock the cloud API instance. const garden = await makeDummyGarden(getDataDir("test-projects", "login", "secret-in-project-variables"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -205,7 +205,7 @@ describe("LoginCommand", () => { const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -236,7 +236,7 @@ describe("LoginCommand", () => { const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -301,7 +301,7 @@ describe("LoginCommand", () => { // this is a bit of a workaround to run outside of the garden root dir const garden = await makeDummyGarden(getDataDir("..", "..", "..", ".."), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -338,7 +338,7 @@ describe("LoginCommand", () => { it("should be a no-op if the user has a valid auth token in the environment", async () => { const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -355,7 +355,7 @@ describe("LoginCommand", () => { it("should throw if the user has an invalid auth token in the environment", async () => { const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -388,7 +388,7 @@ describe("LoginCommand", () => { } const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "missing-domain"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -418,7 +418,7 @@ describe("LoginCommand", () => { } const command = new LoginCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) diff --git a/core/test/unit/src/commands/logout.ts b/core/test/unit/src/commands/logout.ts index 92bffe8886..86b928947d 100644 --- a/core/test/unit/src/commands/logout.ts +++ b/core/test/unit/src/commands/logout.ts @@ -56,7 +56,7 @@ describe("LogoutCommand", () => { const command = new LogOutCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -95,7 +95,7 @@ describe("LogoutCommand", () => { const command = new LogOutCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "missing-domain"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, }) @@ -126,7 +126,7 @@ describe("LogoutCommand", () => { it("should be a no-op if the user is already logged out", async () => { const command = new LogOutCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -147,7 +147,7 @@ describe("LogoutCommand", () => { const command = new LogOutCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -187,7 +187,7 @@ describe("LogoutCommand", () => { const command = new LogOutCommand() const garden = await makeTestGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) @@ -245,7 +245,7 @@ describe("LogoutCommand", () => { // this is a bit of a workaround to run outside of the garden root dir const garden = await makeDummyGarden(getDataDir("..", "..", "..", ".."), { - noEnterprise: false, + skipCloudConnect: false, commandInfo: { name: "foo", args: {}, opts: {} }, globalConfigStore, }) diff --git a/core/test/unit/src/commands/tools.ts b/core/test/unit/src/commands/tools.ts index 87f681bfd2..324af2016f 100644 --- a/core/test/unit/src/commands/tools.ts +++ b/core/test/unit/src/commands/tools.ts @@ -221,7 +221,7 @@ describe("ToolsCommand", () => { it("should run a tool by name when run outside of a project", async () => { const _garden: any = await makeDummyGarden(tmpDir.path, { - noEnterprise: true, + skipCloudConnect: true, commandInfo: { name: "foo", args: {}, opts: {} }, }) _garden.registeredPlugins = [pluginA, pluginB] diff --git a/core/test/unit/src/garden.ts b/core/test/unit/src/garden.ts index 1b1651e0af..a0a7a8d27f 100644 --- a/core/test/unit/src/garden.ts +++ b/core/test/unit/src/garden.ts @@ -71,10 +71,12 @@ import stripAnsi from "strip-ansi" import type { CloudProject } from "../../../src/cloud/api.js" import { CloudApi } from "../../../src/cloud/api.js" import { GlobalConfigStore } from "../../../src/config-store/global.js" -import { getRootLogger } from "../../../src/logger/logger.js" +import { LogLevel, getRootLogger } from "../../../src/logger/logger.js" import { uuidv4 } from "../../../src/util/random.js" import { fileURLToPath } from "node:url" import { resolveMsg } from "../../../src/logger/log-entry.js" +import { getCloudDistributionName } from "../../../src/util/cloud.js" +import { styles } from "../../../src/logger/styles.js" const moduleDirName = dirname(fileURLToPath(import.meta.url)) @@ -596,6 +598,8 @@ describe("Garden", () => { } }) context("user is NOT logged in", () => { + const log = getRootLogger().createLog() + it("should have domain and id if set in project config", async () => { const projectId = uuidv4() const projectName = "test" @@ -631,6 +635,64 @@ describe("Garden", () => { expect(garden.cloudDomain).to.eql(DEFAULT_GARDEN_CLOUD_DOMAIN) expect(garden.projectId).to.eql(undefined) }) + it("should log a warning by default", async () => { + log.root["entries"] = [] + const projectName = "test" + const envName = "default" + const config: ProjectConfig = createProjectConfig({ + name: projectName, + path: pathFoo, + }) + + const garden = await TestGarden.factory(pathFoo, { + config, + environmentString: envName, + log, + }) + const distroName = getCloudDistributionName(garden.cloudDomain || DEFAULT_GARDEN_CLOUD_DOMAIN) + + const expectedLog = log.root.getLogEntries().filter((l) => resolveMsg(l)?.includes(`You are not logged in`)) + + expect(expectedLog.length).to.eql(1) + expect(expectedLog[0].level).to.eql(LogLevel.warn) + expect(expectedLog[0].msg).to.eql( + `You are not logged in. To use ${distroName}, log in with the ${styles.command("garden login")} command.` + ) + }) + context("commands with verbose cloud logs", () => { + const commands = ["dev", "serve", "exit", "quit"] + for (const command of commands) { + it(`should log a warning message at a debug level for command ${command}`, async () => { + log.root["entries"] = [] + const projectName = "test" + const envName = "default" + const config: ProjectConfig = createProjectConfig({ + name: projectName, + path: pathFoo, + }) + + const garden = await TestGarden.factory(pathFoo, { + config, + environmentString: envName, + log, + commandInfo: { + name: command, + args: {}, + opts: {}, + }, + }) + const distroName = getCloudDistributionName(garden.cloudDomain || DEFAULT_GARDEN_CLOUD_DOMAIN) + + const expectedLog = log.root.getLogEntries().filter((l) => resolveMsg(l)?.includes(`You are not logged in`)) + + expect(expectedLog.length).to.eql(1) + expect(expectedLog[0].level).to.eql(LogLevel.debug) + expect(expectedLog[0].msg).to.eql( + `You are not logged in. To use ${distroName}, log in with the ${styles.command("garden login")} command.` + ) + }) + } + }) }) context("user is logged in", () => { let configStoreTmpDir: tmp.DirectoryResult @@ -5262,7 +5324,7 @@ describe("Garden", () => { await garden.emitWarning({ key, log, message }) const logs = getLogMessages(log) expect(logs.length).to.equal(1) - expect(logs[0]).to.equal(message + `\nā†’ Run garden util hide-warning ${key} to disable this warning.`) + expect(logs[0]).to.equal(message + `\nā†’ Run garden util hide-warning ${key} to disable this message.`) }) it("should not log a warning if the key has been hidden", async () => { diff --git a/core/test/unit/src/util/cloud.ts b/core/test/unit/src/util/cloud.ts index f09323ecfe..b9ba21e08b 100644 --- a/core/test/unit/src/util/cloud.ts +++ b/core/test/unit/src/util/cloud.ts @@ -14,8 +14,8 @@ import { expect } from "chai" describe("garden-cloud", () => { describe("getCloudDistributionName", () => { context(`when domain name is ${DEFAULT_GARDEN_CLOUD_DOMAIN}`, () => { - it(`returns "Garden Dashboard" for ${DEFAULT_GARDEN_CLOUD_DOMAIN}`, () => { - expect(getCloudDistributionName(DEFAULT_GARDEN_CLOUD_DOMAIN)).to.eql("Garden Dashboard") + it(`returns "the Garden dashboard" for ${DEFAULT_GARDEN_CLOUD_DOMAIN}`, () => { + expect(getCloudDistributionName(DEFAULT_GARDEN_CLOUD_DOMAIN)).to.eql("the Garden dashboard") }) }) @@ -41,8 +41,8 @@ describe("garden-cloud", () => { }) describe("getCloudLogSectionName", () => { - it(`returns "garden-dashboard" for "Garden Dashboard"`, () => { - expect(getCloudLogSectionName("Garden Dashboard")).to.eql("garden-dashboard") + it(`returns "garden-dashboard" for "the Garden dashboard"`, () => { + expect(getCloudLogSectionName("the Garden dashboard")).to.eql("garden-dashboard") }) it(`returns "garden-cloud" for "Garden Cloud"`, () => {