Commit fcafa6a3 authored by Miguel Rincon's avatar Miguel Rincon Committed by Mayra Cabrera

Add "environment" dropdown to pod logs screen

- Add dropdown to select environment in page
- Display logs of first pod found if no pod is selected
- Add environment_logs_data to display data required on the page
- Update specs
parent 00b6311f
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');
if (this.podName) {
this.podName = _.escape(this.podName); this.podName = _.escape(this.podName);
this.$buildOutputContainer = $(container).find('.js-build-output'); }
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'));
}
})
.finally(() => {
this.$buildRefreshAnimation.hide();
});
} }
createFlash( setupEnvironmentsDropdown(environments) {
sprintf(__(`Something went wrong on our end. %{message}`), { this.setupDropdown(
message, 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);
},
); );
});
} }
populateDropdown(pods) { setupPodsDropdown(pods) {
// set the selected element from the pod set on the url params // Show first pod, it is selected by default
const $podDropdownMenu = this.$podDropdown.find('.dropdown-menu'); 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
.form-group.col-6{ role: 'group' }
%label.d-block.pt-0.col-form-label-sm.col-form-label
= s_('Environments|Environment')
.dropdown.js-environment-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
.form-group.col-6{ role: 'group' }
%label.d-block.pt-0.col-form-label-sm.col-form-label
= s_('Environments|Pod logs from') = s_('Environments|Pod logs from')
.dropdown.js-pod-dropdown.d-flex .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 } %button.dropdown-menu-toggle.d-flex.align-content-center.align-self-center{ type: 'button', data: { toggle: 'dropdown' }, 'aria-expanded': false }
= icon('chevron-down') = icon('chevron-down')
.dropdown-toggle-text
= "&nbsp;".html_safe
.dropdown-menu.dropdown-menu-selectable.dropdown-menu-drop-up .dropdown-menu.dropdown-menu-selectable.dropdown-menu-drop-up
.controllers .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) }
......
This diff is collapsed.
...@@ -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
</label>
<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> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"></div>
</div> </div>
<div class="controllers pull-right">
<div 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 class="form-group col-6" role="group">
<button class="js-scroll-down btn-scroll btn-transparent btn-blank" disabled type="button"></button> <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 class="controllers align-self-end">
<div
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
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 class="refresh-control">
<div
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 class="refresh-control pull-right">
<div class="has-tooltip controllers-buttons" data-container="body" data-placement="top" title="Refresh">
<button class="js-refresh-log btn-default btn-refresh" disabled type="button"></button>
</div> </div>
</div> </div>
</div> </div>
<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> </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>
</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