Commit 7f951780 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '5769-add-environment-drop-down-to-pod-logs' into 'master'

Add "environment" dropdown to pod logs screen

See merge request gitlab-org/gitlab!17532
parents 00b6311f fcafa6a3
import $ from 'jquery'; import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues, redirectTo } from '~/lib/utils/url_utility';
import { isScrolledToBottom, scrollDown, toggleDisableButton } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom, scrollDown, toggleDisableButton } from '~/lib/utils/scroll_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import LogOutputBehaviours from '~/lib/utils/logoutput_behaviours'; import LogOutputBehaviours from '~/lib/utils/logoutput_behaviours';
import createFlash from '~/flash'; import flash from '~/flash';
import { sprintf, __, s__ } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import _ from 'underscore'; import _ from 'underscore';
import { backOff } from '~/lib/utils/common_utils';
const requestWithBackoff = (url, params) =>
backOff((next, stop) => {
axios
.get(url, {
params,
})
.then(res => {
if (!res.data) {
next();
return;
}
stop(res);
})
.catch(err => {
stop(err);
});
});
export default class KubernetesPodLogs extends LogOutputBehaviours { export default class KubernetesPodLogs extends LogOutputBehaviours {
constructor(container) { constructor(container) {
super(); super();
this.options = $(container).data(); this.options = $(container).data();
const { currentEnvironmentName, environmentsPath, logsPath, logsPage } = this.options;
this.environmentName = currentEnvironmentName;
this.environmentsPath = environmentsPath;
this.logsPath = logsPath;
this.logsPage = logsPage;
[this.podName] = getParameterValues('pod_name'); [this.podName] = getParameterValues('pod_name');
this.podName = _.escape(this.podName); if (this.podName) {
this.$buildOutputContainer = $(container).find('.js-build-output'); this.podName = _.escape(this.podName);
}
this.$window = $(window); this.$window = $(window);
this.$buildOutputContainer = $(container).find('.js-build-output');
this.$refreshLogBtn = $(container).find('.js-refresh-log'); this.$refreshLogBtn = $(container).find('.js-refresh-log');
this.$buildRefreshAnimation = $(container).find('.js-build-refresh'); this.$buildRefreshAnimation = $(container).find('.js-build-refresh');
this.isLogComplete = false;
this.$podDropdown = $(container).find('.js-pod-dropdown'); this.$podDropdown = $(container).find('.js-pod-dropdown');
this.$envDropdown = $(container).find('.js-environment-dropdown');
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.isLogComplete = false;
if (!this.podName) {
createFlash(s__('Environments|No pod name has been specified'));
return;
}
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window.off('scroll').on('scroll', () => { this.$window.off('scroll').on('scroll', () => {
if (!isScrolledToBottom()) { if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false); this.toggleScrollAnimation(false);
...@@ -36,7 +62,7 @@ export default class KubernetesPodLogs extends LogOutputBehaviours { ...@@ -36,7 +62,7 @@ export default class KubernetesPodLogs extends LogOutputBehaviours {
this.scrollThrottled(); this.scrollThrottled();
}); });
this.$refreshLogBtn.off('click').on('click', this.getPodLogs.bind(this)); this.$refreshLogBtn.off('click').on('click', this.getData.bind(this));
} }
scrollToBottom() { scrollToBottom() {
...@@ -49,69 +75,122 @@ export default class KubernetesPodLogs extends LogOutputBehaviours { ...@@ -49,69 +75,122 @@ export default class KubernetesPodLogs extends LogOutputBehaviours {
this.toggleScroll(); this.toggleScroll();
} }
getPodLogs() { getData() {
this.scrollToTop(); this.scrollToTop();
this.$buildOutputContainer.empty(); this.$buildOutputContainer.empty();
this.$buildRefreshAnimation.show(); this.$buildRefreshAnimation.show();
toggleDisableButton(this.$refreshLogBtn, 'true'); toggleDisableButton(this.$refreshLogBtn, 'true');
return Promise.all([this.getEnvironments(), this.getLogs()]);
}
getEnvironments() {
return axios return axios
.get(this.options.logsPath, { .get(this.environmentsPath)
params: { pod_name: this.podName }, .then(res => {
const { environments } = res.data;
this.setupEnvironmentsDropdown(environments);
}) })
.catch(() => flash(s__('Environments|An error occurred while fetching the environments.')));
}
getLogs() {
return requestWithBackoff(this.logsPath, { pod_name: this.podName })
.then(res => { .then(res => {
const { logs } = res.data; const { logs, pods } = res.data;
this.populateDropdown(res.data.pods); this.setupPodsDropdown(pods);
const formattedLogs = logs.map(logEntry => `${_.escape(logEntry)} <br />`); this.displayLogs(logs);
this.$buildOutputContainer.append(formattedLogs);
scrollDown();
this.isLogComplete = true;
this.$buildRefreshAnimation.hide();
toggleDisableButton(this.$refreshLogBtn, false);
}) })
.catch(err => { .catch(err => {
let message = ''; const { response } = err;
if (err.response) { if (response && response.status === httpStatusCodes.BAD_REQUEST) {
message = sprintf(`Error: %{message}`, { message: err.response.data.message }); if (response.data && response.data.message) {
flash(
sprintf(
s__('Environments|An error occurred while fetching the logs - Error: %{message}'),
{
message: response.data.message,
},
),
'notice',
);
} else {
flash(
s__(
'Environments|An error occurred while fetching the logs for this environment or pod. Please try again',
),
'notice',
);
}
} else {
flash(__('Environments|An error occurred while fetching the logs'));
} }
})
createFlash( .finally(() => {
sprintf(__(`Something went wrong on our end. %{message}`), { this.$buildRefreshAnimation.hide();
message,
}),
);
}); });
} }
populateDropdown(pods) { setupEnvironmentsDropdown(environments) {
// set the selected element from the pod set on the url params this.setupDropdown(
const $podDropdownMenu = this.$podDropdown.find('.dropdown-menu'); this.$envDropdown,
this.environmentName,
environments.map(({ name, id }) => ({ name, value: id })),
el => {
const envId = el.currentTarget.value;
const envRegexp = /environments\/[0-9]+/gi;
const url = this.logsPage.replace(envRegexp, `environments/${envId}`);
redirectTo(url);
},
);
}
setupPodsDropdown(pods) {
// Show first pod, it is selected by default
this.podName = this.podName || pods[0];
this.setupDropdown(
this.$podDropdown,
this.podName,
pods.map(podName => ({ name: podName, value: podName })),
el => {
const selectedPodName = el.currentTarget.value;
if (selectedPodName !== this.podName) {
this.podName = selectedPodName;
this.getData();
}
},
);
}
this.$podDropdown displayLogs(logs) {
const formattedLogs = logs.map(logEntry => `${_.escape(logEntry)} <br />`);
this.$buildOutputContainer.append(formattedLogs);
scrollDown();
this.isLogComplete = true;
toggleDisableButton(this.$refreshLogBtn, false);
}
setupDropdown($dropdown, activeOption = '', options, onSelect) {
const $dropdownMenu = $dropdown.find('.dropdown-menu');
$dropdown
.find('.dropdown-menu-toggle') .find('.dropdown-menu-toggle')
.html( .html(
`<span class="dropdown-toggle-text text-truncate">${this.podName}</span><i class="fa fa-chevron-down"></i>`, `<span class="dropdown-toggle-text text-truncate">${activeOption}</span><i class="fa fa-chevron-down"></i>`,
); );
$podDropdownMenu.off('click');
$podDropdownMenu.empty();
pods.forEach(pod => { $dropdownMenu.off('click');
$podDropdownMenu.append(` $dropdownMenu.empty();
<button class='dropdown-item'>
${_.escape(pod)} options.forEach(option => {
$dropdownMenu.append(`
<button class='dropdown-item' value='${option.value}'>
${_.escape(option.name)}
</button> </button>
`); `);
}); });
$podDropdownMenu.find('button').on('click', this.changePodLog.bind(this)); $dropdownMenu.find('button').on('click', onSelect.bind(this));
}
changePodLog(el) {
const selectedPodName = el.currentTarget.textContent.trim();
if (selectedPodName !== this.podName) {
this.podName = selectedPodName;
this.getPodLogs();
}
} }
} }
...@@ -4,5 +4,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -4,5 +4,5 @@ document.addEventListener('DOMContentLoaded', () => {
const kubernetesLogContainer = document.querySelector('.js-kubernetes-logs'); const kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
const kubernetesLog = new KubernetesLogs(kubernetesLogContainer); const kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs(); kubernetesLog.getData();
}); });
...@@ -8,14 +8,7 @@ ...@@ -8,14 +8,7 @@
} }
.top-bar { .top-bar {
@include build-trace-top-bar(48px); @include build-trace-top-bar($gl-line-height * 5);
display: flex;
.truncated-info {
display: flex;
justify-content: center;
align-items: center;
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 200px; width: 200px;
......
...@@ -30,6 +30,15 @@ module EE ...@@ -30,6 +30,15 @@ module EE
project.feature_available?(:custom_prometheus_metrics) && can?(current_user, :admin_project, project) project.feature_available?(:custom_prometheus_metrics) && can?(current_user, :admin_project, project)
end end
def environment_logs_data(project, environment)
{
"current-environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json),
"logs-path": logs_project_environment_path(project, environment, format: :json),
"logs-page": logs_project_environment_path(project, environment)
}
end
def metrics_data(project, environment) def metrics_data(project, environment)
ee_metrics_data = { ee_metrics_data = {
"custom-metrics-path" => project_prometheus_metrics_path(project), "custom-metrics-path" => project_prometheus_metrics_path(project),
......
.js-kubernetes-logs{ data: { logs_path: logs_project_environment_path(@project, @environment, format: :json) } } .js-kubernetes-logs{ data: environment_logs_data(@project, @environment) }
.build-page-pod-logs .build-page-pod-logs
.build-trace-container.prepend-top-default .build-trace-container.prepend-top-default
.top-bar.js-top-bar .top-bar.js-top-bar.d-flex
.truncated-info.d-none.d-md-flex.append-right-8 .row
= s_('Environments|Pod logs from') .form-group.col-6{ role: 'group' }
.dropdown.js-pod-dropdown.d-flex %label.d-block.pt-0.col-form-label-sm.col-form-label
%button.dropdown-menu-toggle.d-flex.align-content-center.align-self-center{ type: 'button', data: { toggle: 'dropdown' }, 'aria-expanded': false } = s_('Environments|Environment')
= icon('chevron-down') .dropdown.js-environment-dropdown.d-flex
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-drop-up %button.dropdown-menu-toggle.d-flex.align-content-center.align-self-center{ type: 'button', data: { toggle: 'dropdown' }, 'aria-expanded': false }
.controllers = icon('chevron-down')
.dropdown-toggle-text
= "&nbsp;".html_safe
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-drop-up
.form-group.col-6{ role: 'group' }
%label.d-block.pt-0.col-form-label-sm.col-form-label
= s_('Environments|Pod logs from')
.dropdown.js-pod-dropdown.d-flex
%button.dropdown-menu-toggle.d-flex.align-content-center.align-self-center{ type: 'button', data: { toggle: 'dropdown' }, 'aria-expanded': false }
= icon('chevron-down')
.dropdown-toggle-text
= "&nbsp;".html_safe
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-drop-up
.controllers.align-self-end
.has-tooltip.controllers-buttons{ title: _('Scroll to top'), data: { placement: 'top', container: 'body'} } .has-tooltip.controllers-buttons{ title: _('Scroll to top'), data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up') = custom_icon('scroll_up')
......
---
title: Add environment dropdown to pod logs screen
merge_request: 17532
author:
type: changed
...@@ -31,6 +31,24 @@ describe EnvironmentsHelper do ...@@ -31,6 +31,24 @@ describe EnvironmentsHelper do
end end
end end
describe '#environment_logs_data' do
subject { helper.environment_logs_data(project, environment) }
it 'returns environment parameters data' do
expect(subject).to include(
"current-environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json)
)
end
it 'returns logs parameters data' do
expect(subject).to include(
"logs-path": logs_project_environment_path(project, environment, format: :json),
"logs-page": logs_project_environment_path(project, environment)
)
end
end
describe '#custom_metrics_available?' do describe '#custom_metrics_available?' do
subject { helper.custom_metrics_available?(project) } subject { helper.custom_metrics_available?(project) }
......
...@@ -2,46 +2,101 @@ import $ from 'jquery'; ...@@ -2,46 +2,101 @@ import $ from 'jquery';
import KubernetesLogs from 'ee/kubernetes_logs'; import KubernetesLogs from 'ee/kubernetes_logs';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { logMockData, podMockData } from './kubernetes_mock_data'; import { logMockData, podMockData, mockEnvironmentData } from './kubernetes_mock_data';
describe('Kubernetes Logs', () => { describe('Kubernetes Logs', () => {
const fixtureTemplate = 'static/environments_logs.html'; const fixtureTemplate = 'static/environments_logs.html';
const mockPodName = 'production-tanuki-1'; let mockDataset;
const logMockPath = '/root/kubernetes-app/environments/1/logs';
let kubernetesLogContainer; let kubernetesLogContainer;
let kubernetesLog; let kubernetesLog;
let mock; let mock;
let mockFlash;
preloadFixtures(fixtureTemplate); preloadFixtures(fixtureTemplate);
describe('When data is requested correctly', () => { describe('When data is requested correctly', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [mockPodName]); spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => []);
mockFlash = spyOnDependency(KubernetesLogs, 'flash').and.callFake(() => []);
mock = new MockAdapter(axios); kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
mock.onGet(logMockPath).reply(200, { logs: logMockData, pods: podMockData }); mockDataset = kubernetesLogContainer.dataset;
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs'); mock = new MockAdapter(axios);
mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData });
mock.onGet(mockDataset.logsPath).reply(200, { logs: logMockData, pods: podMockData });
}); });
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); });
it('has the pod name placed on the dropdown', done => { it('has the environment name placed on the dropdown', done => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
const dropdown = document
.querySelector('.js-environment-dropdown')
.querySelector('.dropdown-menu-toggle');
expect(dropdown.textContent).toContain(mockDataset.currentEnvironmentName);
done();
})
.catch(done.fail);
});
it('loads all environments as options of their dropdown', done => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
const options = document
.querySelector('.js-environment-dropdown')
.querySelectorAll('.dropdown-item');
expect(options.length).toEqual(mockEnvironmentData.length);
options.forEach((item, i) => {
expect(item.textContent.trim()).toBe(mockEnvironmentData[i].name);
});
done();
})
.catch(done.fail);
});
it('loads all pod names as options of their dropdown', done => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs(); kubernetesLog
.getData()
.then(() => {
const options = document
.querySelector('.js-pod-dropdown')
.querySelectorAll('.dropdown-item');
setTimeout(() => { expect(options.length).toEqual(podMockData.length);
const podDropdown = document options.forEach((item, i) => {
.querySelector('.js-pod-dropdown') expect(item.textContent.trim()).toBe(podMockData[i]);
.querySelector('.dropdown-menu-toggle'); });
done();
})
.catch(done.fail);
});
it('has the pod name placed on the dropdown', done => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
const podDropdown = document
.querySelector('.js-pod-dropdown')
.querySelector('.dropdown-menu-toggle');
expect(podDropdown.textContent).toContain(mockPodName); expect(podDropdown.textContent).toContain(podMockData[0]);
done(); done();
}, 0); })
.catch(done.fail);
}); });
it('queries the pod log data and sets the dom elements', done => { it('queries the pod log data and sets the dom elements', done => {
...@@ -49,41 +104,88 @@ describe('Kubernetes Logs', () => { ...@@ -49,41 +104,88 @@ describe('Kubernetes Logs', () => {
const toggleDisableSpy = spyOnDependency(KubernetesLogs, 'toggleDisableButton').and.stub(); const toggleDisableSpy = spyOnDependency(KubernetesLogs, 'toggleDisableButton').and.stub();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs(); expect(kubernetesLog.isLogComplete).toEqual(false);
setTimeout(() => {
expect(kubernetesLog.isLogComplete).toEqual(true); kubernetesLog
expect(kubernetesLog.$buildOutputContainer.text()).toContain(logMockData[0].trim()); .getData()
expect(scrollSpy).toHaveBeenCalled(); .then(() => {
expect(toggleDisableSpy).toHaveBeenCalled(); expect(kubernetesLog.isLogComplete).toEqual(true);
done();
}, 0); expect(document.querySelector('.js-build-output').textContent).toContain(
logMockData[0].trim(),
);
expect(scrollSpy).toHaveBeenCalled();
expect(toggleDisableSpy).toHaveBeenCalled();
done();
})
.catch(done.fail);
}); });
it('asks for the pod logs from another pod', done => { it('asks for the pod logs from another pod', done => {
const changePodLogSpy = spyOn(KubernetesLogs.prototype, 'getPodLogs').and.callThrough(); const changePodLogSpy = spyOn(KubernetesLogs.prototype, 'getData').and.callThrough();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs(); kubernetesLog
setTimeout(() => { .getData()
const podDropdown = document.querySelectorAll('.js-pod-dropdown .dropdown-menu button'); .then(() => {
const anotherPod = podDropdown[podDropdown.length - 1]; const podDropdown = document.querySelectorAll('.js-pod-dropdown .dropdown-menu button');
const anotherPod = podDropdown[podDropdown.length - 1];
anotherPod.click(); anotherPod.click();
expect(changePodLogSpy.calls.count()).toEqual(2); expect(changePodLogSpy.calls.count()).toEqual(2);
done(); done();
}, 0); })
.catch(done.fail);
}); });
it('clears the pod dropdown contents when pod logs are requested', done => { it('clears the pod dropdown contents when pod logs are requested', done => {
const emptySpy = spyOn($.prototype, 'empty').and.callThrough(); const emptySpy = spyOn($.prototype, 'empty').and.callThrough();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
// 3 elems should be emptied:
// 1. the environment dropdown items
// 2. the pods dropdown items
// 3. the job log contents
expect(emptySpy.calls.count()).toEqual(3);
done();
})
.catch(done.fail);
});
kubernetesLog.getPodLogs(); describe('shows an alert', () => {
setTimeout(() => { it('with an error', done => {
// This is because it clears both the job log contents and the dropdown mock.onGet(mockDataset.logsPath).reply(400);
expect(emptySpy.calls.count()).toEqual(2);
done(); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
expect(mockFlash.calls.count()).toEqual(1);
done();
})
.catch(done.fail);
});
it('with some explicit error', done => {
const errorMsg = 'Some k8s error';
mock.onGet(mockDataset.logsPath).reply(400, {
message: errorMsg,
});
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
expect(mockFlash.calls.count()).toEqual(1);
expect(mockFlash.calls.argsFor(0).join()).toContain(errorMsg);
done();
})
.catch(done.fail);
}); });
}); });
}); });
...@@ -92,14 +194,11 @@ describe('Kubernetes Logs', () => { ...@@ -92,14 +194,11 @@ describe('Kubernetes Logs', () => {
const hackyPodName = '">&lt;img src=x onerror=alert(document.domain)&gt; production'; const hackyPodName = '">&lt;img src=x onerror=alert(document.domain)&gt; production';
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [hackyPodName]); spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [hackyPodName]);
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockDataset.logsPath).reply(200, { logs: logMockData, pods: [hackyPodName] });
mock.onGet(logMockPath).reply(200, { logs: logMockData, pods: [hackyPodName] });
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
}); });
afterEach(() => { afterEach(() => {
...@@ -115,18 +214,81 @@ describe('Kubernetes Logs', () => { ...@@ -115,18 +214,81 @@ describe('Kubernetes Logs', () => {
}); });
}); });
describe('When no pod name is available', () => { describe('When data is not yet loaded into cache', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [podMockData[1]]);
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
// override setTimeout, to simulate polling
const origSetTimeout = window.setTimeout;
spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
mockDataset = kubernetesLogContainer.dataset;
mock = new MockAdapter(axios);
mock.onGet(mockDataset.environmentsPath).reply(200, { environments: mockEnvironmentData });
// Simulate reactive cache, 2 tries needed
mock.onGet(`${mockDataset.logsPath}`, { pod_name: podMockData[1] }).replyOnce(202);
mock
.onGet(`${mockDataset.logsPath}`, { pod_name: podMockData[1] })
.reply(200, { logs: logMockData, pods: podMockData });
});
it('queries the pod log data polling for reactive cache', done => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
expect(kubernetesLog.isLogComplete).toEqual(false);
kubernetesLog
.getData()
.then(() => {
const calls = mock.history.get.filter(r => r.url === mockDataset.logsPath);
// expect 2 tries
expect(calls.length).toEqual(2);
expect(calls[0].params).toEqual({ pod_name: podMockData[1] });
expect(calls[1].params).toEqual({ pod_name: podMockData[1] });
expect(document.querySelector('.js-build-output').textContent).toContain(
logMockData[0].trim(),
);
done();
})
.catch(done.fail);
});
afterEach(() => {
mock.restore();
});
});
describe('When data is requested with a pod name', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [podMockData[2]]);
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs'); kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
mock = new MockAdapter(axios);
}); });
it('shows up a flash message when no pod name is specified', () => { it('logs are loaded with the correct pod_name parameter', done => {
const createFlashSpy = spyOnDependency(KubernetesLogs, 'createFlash').and.stub();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer); kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog
.getData()
.then(() => {
const logsCall = mock.history.get.filter(call => call.url === mockDataset.logsPath);
expect(createFlashSpy).toHaveBeenCalled(); expect(logsCall.length).toBe(1);
expect(logsCall[0].params.pod_name).toEqual(podMockData[2]);
done();
})
.catch(done.fail);
});
afterEach(() => {
mock.restore();
}); });
}); });
}); });
...@@ -104,6 +104,21 @@ export const logMockData = [ ...@@ -104,6 +104,21 @@ export const logMockData = [
'- -> /', '- -> /',
]; ];
export const podMockData = ['production-tanuki-1', 'production-tanuki-2']; export const podMockData = ['production-tanuki-1', 'production-tanuki-2', 'production-tanuki-3'];
export const mockEnvironmentData = [
{
name: 'production',
id: 1,
},
{
name: 'stating',
id: 2,
},
{
name: 'review/my-new-branch',
id: 3,
},
];
export default {}; export default {};
...@@ -6030,6 +6030,15 @@ msgstr "" ...@@ -6030,6 +6030,15 @@ msgstr ""
msgid "Environments|An error occurred while fetching the environments." msgid "Environments|An error occurred while fetching the environments."
msgstr "" msgstr ""
msgid "Environments|An error occurred while fetching the logs"
msgstr ""
msgid "Environments|An error occurred while fetching the logs - Error: %{message}"
msgstr ""
msgid "Environments|An error occurred while fetching the logs for this environment or pod. Please try again"
msgstr ""
msgid "Environments|An error occurred while making the request." msgid "Environments|An error occurred while making the request."
msgstr "" msgstr ""
...@@ -6075,9 +6084,6 @@ msgstr "" ...@@ -6075,9 +6084,6 @@ msgstr ""
msgid "Environments|No deployments yet" msgid "Environments|No deployments yet"
msgstr "" msgstr ""
msgid "Environments|No pod name has been specified"
msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file." msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
msgstr "" msgstr ""
...@@ -14986,9 +14992,6 @@ msgstr "" ...@@ -14986,9 +14992,6 @@ msgstr ""
msgid "Something went wrong on our end." msgid "Something went wrong on our end."
msgstr "" msgstr ""
msgid "Something went wrong on our end. %{message}"
msgstr ""
msgid "Something went wrong on our end. Please try again!" msgid "Something went wrong on our end. Please try again!"
msgstr "" msgstr ""
......
<div class="js-kubernetes-logs" data-logs-path="/root/kubernetes-app/environments/1/logs"> <div
<div class="build-page"> class="js-kubernetes-logs"
data-current-environment-name="production"
data-environments-path="/root/my-project/environments.json"
data-logs-page="/root/my-project/environments/1/logs"
data-logs-path="/root/my-project/environments/1/logs.json"
>
<div class="build-page-pod-logs">
<div class="build-trace-container prepend-top-default"> <div class="build-trace-container prepend-top-default">
<div class="top-bar js-top-bar"> <div class="top-bar js-top-bar d-flex">
<div class="truncated-info hidden-xs pull-left"></div> <div class="row">
<div class="dropdown prepend-left-10 js-pod-dropdown"> <div class="form-group col-6" role="group">
<button aria-expanded="false" class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> <label class="d-block col-form-label-sm col-form-label">
<i class="fa fa-chevron-down"></i> Environment
</button> </label>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div> <div class="dropdown js-environment-dropdown d-flex">
<button
aria-expanded="false"
class="dropdown-menu-toggle d-flex align-content-center align-self-center"
data-toggle="dropdown"
type="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
<div class="dropdown-toggle-text">
&nbsp;
</div>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
</div>
</div>
<div class="form-group col-6" role="group">
<label class="d-block col-form-label-sm col-form-label">
Pod logs from
</label>
<div class="dropdown js-pod-dropdown d-flex">
<button
aria-expanded="false"
class="dropdown-menu-toggle d-flex align-content-center align-self-center"
data-toggle="dropdown"
type="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
<div class="dropdown-toggle-text">
&nbsp;
</div>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
</div>
</div>
</div> </div>
<div class="controllers pull-right"> <div class="controllers align-self-end">
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to top"> <div
<button class="js-scroll-up btn-scroll btn-transparent btn-blank" disabled type="button"></button> class="has-tooltip controllers-buttons"
data-container="body"
data-placement="top"
title="Scroll to top"
>
<button
class="js-scroll-up btn-scroll btn-transparent btn-blank"
disabled
type="button"
></button>
</div> </div>
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Scroll to bottom"> <div
<button class="js-scroll-down btn-scroll btn-transparent btn-blank" disabled type="button"></button> class="has-tooltip controllers-buttons"
data-container="body"
data-placement="top"
title="Scroll to bottom"
>
<button
class="js-scroll-down btn-scroll btn-transparent btn-blank"
disabled
type="button"
></button>
</div> </div>
<div class="refresh-control pull-right"> <div class="refresh-control">
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Refresh"> <div
<button class="js-refresh-log btn-default btn-refresh" disabled type="button"></button> class="has-tooltip controllers-buttons"
data-container="body"
data-placement="top"
title="Refresh"
>
<button
class="js-refresh-log btn btn-default btn-refresh h-32-px"
disabled
type="button"
></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<pre class="build-trace" id="build-trace"><code class="bash js-build-output"><div class="build-loader-animation js-build-refresh"></div></code></pre> <pre class="build-trace" id="build-trace">
<code class="bash js-build-output"></code>
<div class="build-loader-animation js-build-refresh">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</pre>
</div> </div>
</div> </div>
</div> </div>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment