Skip to content

Commit

Permalink
refactor: reads GUI base path from DOM (#474)
Browse files Browse the repository at this point in the history
Adds a `<base>` tag to the index.html file which will be set to an actual base path during development and the GO template placeholder `{{.BaseGuiPath}}` for production. Upon serving the GUI application, the placeholder will be replaced with the actual GUI base path.

Adds the `kuma-config` script tag containing `{{.}}` to the index.html file which will be replaced via Go templating with a basic data structure that, among other things, will hold the actual API base URL and the GUI base path.

Reads the API base URL and the GUI base path from the index.html file replacing any attempt of determining it from the current URL which is error-prone and inflexible.

Signed-off-by: Philipp Rudloff <philipp.rudloff@konghq.com>
  • Loading branch information
kleinfreund committed Dec 1, 2022
1 parent 2d46751 commit 6d56199
Show file tree
Hide file tree
Showing 28 changed files with 426 additions and 232 deletions.
5 changes: 2 additions & 3 deletions .env
Expand Up @@ -2,8 +2,7 @@ VITE_DATA_TIMEOUT=500
VITE_INSTALL_URL=https://kuma.io/install/latest/
VITE_VERSION_URL=https://kuma.io/latest_version/
VITE_NAMESPACE=Kuma
VITE_KUMA_API_SERVER_URL=http://localhost:5681/dev/
VITE_KUMA_DP_SERVER_URL=https://localhost:5678/
VITE_KUMA_API_SERVER_URL=http://localhost:5681
VITE_KUMA_DP_SERVER_URL=https://localhost:5678
VITE_AMCHARTS_LICENSE=''
# utm values
VITE_UTM=?utm_source=Kuma&utm_medium=Kuma-GUI
37 changes: 22 additions & 15 deletions index.html
@@ -1,21 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />

<link rel="icon" href="/favicon.png" />
<title>Manager</title>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- This trailing slash matters! Do not remove it. See also: https://stackoverflow.com/a/26043021/2036825 -->
<base href="<%- baseGuiPath %>/" />

<script type="module" src="/src/main.ts"></script>
</head>
<body>
<noscript>
<strong>
We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.
</strong>
</noscript>
<link rel="icon" href="/favicon.png" />
<title>Manager</title>

<script type="application/json" id="kuma-config"><%- config %></script>

<script type="module" src="/src/main.ts"></script>
</head>

<body>
<noscript>
<strong>
We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.
</strong>
</noscript>

<div id="app"></div>
</body>

<div id="app"></div>
</body>
</html>
4 changes: 0 additions & 4 deletions jest/jest-setup-after-env.ts
Expand Up @@ -46,12 +46,8 @@ expect.addSnapshotSerializer(replaceAttributesSnapshotSerializer([

const server = setupMockServer(import.meta.env.VITE_KUMA_API_SERVER_URL)

// Establish API mocking before all tests.
beforeAll(() => server.listen())
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())

export { router, server }
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -10,7 +10,7 @@
"dev:real-api": "vite",
"preview": "vite preview",
"build": "vite build",
"build:ci": "cross-env VITE_MOCK_API_ENABLED=true VITE_FAKE_MULTIZONE=true yarn run build",
"build:ci": "cross-env VITE_MOCK_API_ENABLED=true VITE_FAKE_MULTIZONE=true yarn run build --mode preview",
"lint": "eslint --ext .js,.ts,.vue --fix .",
"lint:lockfile": "lockfile-lint --path yarn.lock --allowed-hosts yarn --validate-https",
"test": "cross-env TZ=UTC jest",
Expand Down Expand Up @@ -73,6 +73,7 @@
"ts-jest": "^29.0.3",
"typescript": "~4.9.3",
"vite": "^3.2.4",
"vite-plugin-html": "^3.2.0",
"vite-plugin-rewrite-all": "^1.0.0",
"vite-svg-loader": "^3.6.0"
},
Expand Down
4 changes: 4 additions & 0 deletions public/_redirects
@@ -0,0 +1,4 @@
# Only used for Netlify deployment previews.
# Redirects all requests back to index.html to make vue router’s history mode work in Netlify environments (see https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode).

/* /index.html 200
28 changes: 28 additions & 0 deletions src/api/RestClient.spec.ts
@@ -0,0 +1,28 @@
import { describe, expect, test } from '@jest/globals'

import { RestClient } from './RestClient'

describe('RestClient', () => {
test('has expected initial base URL', () => {
const restClient = new RestClient()

expect(restClient.baseUrl).toBe('http://localhost:5681')
})

test.each([
['api', 'http://localhost:5681/api'],
['/api', 'http://localhost:5681/api'],
['/api/', 'http://localhost:5681/api'],
['test/api', 'http://localhost:5681/test/api'],
['/test/api', 'http://localhost:5681/test/api'],
['/test/api/', 'http://localhost:5681/test/api'],
['http://localhost:1234/api', 'http://localhost:1234/api'],
['http://localhost:1234/api/', 'http://localhost:1234/api'],
])('sets expected base URL for “%s”', (newBaseUrl, expectedBaseUrl) => {
const restClient = new RestClient()

restClient.baseUrl = newBaseUrl

expect(restClient.baseUrl).toBe(expectedBaseUrl)
})
})
56 changes: 22 additions & 34 deletions src/api/RestClient.ts
@@ -1,46 +1,30 @@
import { getAppBaseUrl } from '@/utilities/getAppBaseUrl'
import { makeRequest } from './makeRequest'

const DEFAULT_BASE_URL = import.meta.env.PROD ? window.location.origin : import.meta.env.VITE_KUMA_API_SERVER_URL

export class RestClient {
/**
* The API origin. **Never has a trailing slash**.
* The API base URL. **Will always be stored without a trailing slash**.
*/
_origin: string
_baseUrl: string = DEFAULT_BASE_URL

/**
* The base API path relative to the API origin. **Never has a leading or trailing slash**.
* The absolute API base URL used in all requests. Includes its base path segment if one is set.
*/
_basePath: string
get baseUrl() {
return this._baseUrl
}

/**
* @param basePath **Default: `''`**. A base path under which the client’s API is served (e.g. `'api'`). Leading and trailing slashes will be ignored.
* @param baseUrlOrPath the API base URL or the API base path
*/
constructor(basePath: string = '') {
let origin

if (import.meta.env.PROD) {
origin = getAppBaseUrl(window.location)
set baseUrl(baseUrlOrPath: string) {
if (baseUrlOrPath.startsWith('http')) {
this._baseUrl = trimTrailingSlashes(baseUrlOrPath)
} else {
origin = import.meta.env.VITE_KUMA_API_SERVER_URL
const basePath = trimSlashes(baseUrlOrPath)
this._baseUrl = [DEFAULT_BASE_URL, basePath].filter((segment) => segment !== '').join('/')
}

this._origin = trimTrailingSlashes(origin)
this._basePath = trimBoundarySlashes(basePath)
}

/**
* The absolute API URL used in all requests. Includes its base path segment if one is set.
*/
get url() {
return [this._origin, this.basePath].filter((segment) => segment !== '').join('/')
}

get basePath() {
return this._basePath
}

set basePath(basePath: string) {
this._basePath = trimBoundarySlashes(basePath)
}

/**
Expand All @@ -62,8 +46,8 @@ export class RestClient {
*
* @returns the response’s de-serialized data (when applicable) and the raw `Response` object.
*/
async raw(path: string, options?: RequestInit & { params?: any }): Promise<{ response: Response, data: any }> {
const url = path.startsWith('http') ? path : `${this.url}/${path}`
async raw(urlOrPath: string, options?: RequestInit & { params?: any }): Promise<{ response: Response, data: any }> {
const url = urlOrPath.startsWith('http') ? urlOrPath : [this.baseUrl, urlOrPath].join('/')

return makeRequest(url, options)
}
Expand All @@ -73,6 +57,10 @@ function trimTrailingSlashes(str: string): string {
return str.replace(/\/+$/, '')
}

function trimBoundarySlashes(str: string): string {
return str.replace(/^\/+/, '').replace(/\/+$/, '')
function trimLeadingSlashes(str: string): string {
return str.replace(/^\/+/, '')
}

function trimSlashes(str: string): string {
return trimTrailingSlashes(trimLeadingSlashes(str))
}
16 changes: 7 additions & 9 deletions src/api/kumaApi.ts
Expand Up @@ -25,17 +25,17 @@ class KumaApi {
this.client = new RestClient()
}

get url() {
return this.client.url
get baseUrl() {
return this.client.baseUrl
}

/**
* Sets the API base path for all network requests.
* Sets the API base URL for all network requests.
*
* URLs for requests will be constructed in the form `${origin}/${basePath}/${path}`.
* URLs for requests will be constructed in the form `${baseUrl}/${path}`.
*/
setBasePath(basePath: string): void {
this.client.basePath = basePath
setBaseUrl(baseUrl: string): void {
this.client.baseUrl = baseUrl
}

getInfo(): Promise<Info> {
Expand All @@ -53,9 +53,7 @@ class KumaApi {
}

async getLatestVersion(): Promise<string> {
const url = String(import.meta.env.VITE_VERSION_URL)

return this.client.get(url)
return this.client.get(import.meta.env.VITE_VERSION_URL)
}

getConfig(): Promise<ClientConfigInterface> {
Expand Down
2 changes: 1 addition & 1 deletion src/api/mocks.ts
Expand Up @@ -156,7 +156,7 @@ const mockFileImports: Array<[string, () => Promise<any>]> = [
['meshes/:mesh/dataplanes/:dataplaneName/xds', () => import('./mock-data/dataplane-xds.json')],
]

export function setupHandlers(url: string): RestHandler[] {
export function setupHandlers(url: string = ''): RestHandler[] {
const origin = url.replace(/\/+$/, '')

function getApiPath(path: string = '') {
Expand Down
2 changes: 1 addition & 1 deletion src/api/setupMockServer.ts
Expand Up @@ -7,6 +7,6 @@ import { setupHandlers, additionalTestHandlers } from './mocks'
*
* **IMPORTANT**: Do not import this file in the regular application. Since it imports `msw/node`, this will cause the application to break because it will try to import (require, actually) Node built-ins which aren’t available in browser environments.
*/
export function setupMockServer(url: string): SetupServerApi {
export function setupMockServer(url: string = ''): SetupServerApi {
return setupServer(...setupHandlers(url), ...additionalTestHandlers)
}
2 changes: 1 addition & 1 deletion src/app/AppErrorMessage.vue
Expand Up @@ -17,7 +17,7 @@

<template #message>
<p>
Please double check to make sure it is up and running <span v-if="kumaApi.url">, and it is reachable at <code>{{ kumaApi.url }}</code></span>
Please double check to make sure it is up and running <span v-if="kumaApi.baseUrl">, and it is reachable at <code>{{ kumaApi.baseUrl }}</code></span>
</p>
</template>
</KEmptyState>
Expand Down
6 changes: 3 additions & 3 deletions src/app/common/EnvoyData.spec.ts
Expand Up @@ -23,7 +23,7 @@ describe('EnvoyData.vue', () => {

test('renders snapshot', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
res(ctx.status(200), ctx.json('')),
),
)
Expand All @@ -38,7 +38,7 @@ describe('EnvoyData.vue', () => {

test('renders loading', () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
res(ctx.status(200), ctx.json('')),
),
)
Expand All @@ -54,7 +54,7 @@ describe('EnvoyData.vue', () => {

test('renders error', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/dataplanes/:dataplaneName/clusters', (req, res, ctx) =>
res(ctx.status(500), ctx.json('')),
),
)
Expand Down
14 changes: 3 additions & 11 deletions src/app/data-planes/components/DataplanePolicies.spec.ts
@@ -1,4 +1,4 @@
import { describe, expect, jest, test } from '@jest/globals'
import { describe, expect, test } from '@jest/globals'
import { flushPromises, mount, RouterLinkStub } from '@vue/test-utils'
import { rest } from 'msw'

Expand Down Expand Up @@ -37,12 +37,6 @@ describe('DataplanePolicies.vue', () => {
})

test('renders loading', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/policies', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ total: 0, items: [] })),
),
)

const wrapper = await renderComponent({
dataPlane: {
mesh: 'foo',
Expand All @@ -55,10 +49,8 @@ describe('DataplanePolicies.vue', () => {
})

test('renders error', async () => {
jest.spyOn(console, 'error').mockImplementation(() => { }) // silence console errors

server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/policies', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/dataplanes/:dataplaneName/policies', (req, res, ctx) =>
res(ctx.status(500), ctx.json({})),
),
)
Expand All @@ -78,7 +70,7 @@ describe('DataplanePolicies.vue', () => {

test('renders no item', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName/policies', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/dataplanes/:dataplaneName/policies', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ total: 0, items: [] })),
),
)
Expand Down
4 changes: 2 additions & 2 deletions src/app/policies/components/PolicyConnections.spec.ts
Expand Up @@ -55,7 +55,7 @@ describe('PolicyConnections.vue', () => {

test('renders error', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/:policyType/:policyName/dataplanes', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/:policyType/:policyName/dataplanes', (req, res, ctx) =>
res(ctx.status(500), ctx.json({})),
),
)
Expand All @@ -73,7 +73,7 @@ describe('PolicyConnections.vue', () => {

test('renders no item', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/:policyType/:policyName/dataplanes', (req, res, ctx) =>
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + '/meshes/:mesh/:policyType/:policyName/dataplanes', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ total: 0, items: [] })),
),
)
Expand Down
3 changes: 0 additions & 3 deletions src/app/policies/views/PolicyView.spec.ts
Expand Up @@ -21,15 +21,12 @@ describe('PolicyView', () => {
await flushPromises()

const documentationLink = wrapper.find('[data-testid="policy-documentation-link"]')

expect(documentationLink.exists()).toBe(true)

const singleEntity = wrapper.find('[data-testid="policy-single-entity"]')

expect(singleEntity.html()).toContain('Circuit Breaker: cb1')

const policyOverview = wrapper.find('[data-testid="policy-overview-tab"]')

expect(policyOverview.html()).toContain('CircuitBreaker')
})
})
8 changes: 0 additions & 8 deletions src/app/wizard/views/DataplaneUniversal.spec.ts
@@ -1,10 +1,8 @@
import { beforeEach, describe, expect, jest, test } from '@jest/globals'
import { flushPromises, mount } from '@vue/test-utils'
import { rest } from 'msw'

import DataplaneUniversal from './DataplaneUniversal.vue'
import { store } from '@/store/store'
import { server } from '@/../jest/jest-setup-after-env'

function renderComponent() {
return mount(DataplaneUniversal)
Expand All @@ -16,12 +14,6 @@ describe('DataplaneUniversal.vue', () => {
})

test('passes whole wizzard and render yaml', async () => {
server.use(
rest.get(import.meta.env.VITE_KUMA_API_SERVER_URL + 'meshes/:mesh/dataplanes/:dataplaneName', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ name: 'hi' })),
),
)

store.state.config.tagline = import.meta.env.VITE_NAMESPACE
store.state.meshes.items = [
{
Expand Down

0 comments on commit 6d56199

Please sign in to comment.