Commit 087f9155 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '25483-serverless-empty-state' into 'master'

Serverless empty state update

Closes #25483

See merge request gitlab-org/gitlab!36762
parents 0a8fe94b 45c8d030
<script> <script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex';
export default { export default {
props: { components: {
clustersPath: { GlEmptyState,
type: String, GlLink,
required: true, GlSprintf,
}, },
helpPath: { computed: {
type: String, ...mapState(['clustersPath', 'emptyImagePath', 'helpPath']),
required: true,
},
}, },
}; };
</script> </script>
<template> <template>
<div class="row empty-state js-empty-state"> <gl-empty-state
<div class="col-12"> :svg-path="emptyImagePath"
<div class="text-content"> :title="s__('Serverless|Getting started with serverless')"
<h4 class="state-title text-center"> :primary-button-link="clustersPath"
{{ s__('Serverless|Getting started with serverless') }} :primary-button-text="s__('Serverless|Install Knative')"
</h4> >
<p class="state-description"> <template #description>
{{ <gl-sprintf
s__(`Serverless| In order to start using functions as a service, :message="
you must first install Knative on your Kubernetes cluster.`) s__(
}} 'Serverless|In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. %{linkStart}More information%{linkEnd}',
)
<a :href="helpPath"> {{ __('More information') }} </a> "
</p> >
<template #link="{ content }">
<div class="text-center"> <gl-link :href="helpPath">{{ content }}</gl-link>
<a :href="clustersPath" class="btn btn-success"> </template>
{{ s__('Serverless|Install Knative') }} </gl-sprintf>
</a> </template>
</div> </gl-empty-state>
</div>
</div>
</div>
</template> </template>
...@@ -23,14 +23,6 @@ export default { ...@@ -23,14 +23,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -96,8 +88,6 @@ export default { ...@@ -96,8 +88,6 @@ export default {
<area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" /> <area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
<missing-prometheus <missing-prometheus
v-if="!hasPrometheus || hasPrometheusMissingData" v-if="!hasPrometheus || hasPrometheusMissingData"
:help-path="helpPath"
:clusters-path="clustersPath"
:missing-data="hasPrometheusMissingData" :missing-data="hasPrometheusMissingData"
/> />
</section> </section>
......
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import EnvironmentRow from './environment_row.vue'; import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
...@@ -10,24 +10,11 @@ export default { ...@@ -10,24 +10,11 @@ export default {
components: { components: {
EnvironmentRow, EnvironmentRow,
EmptyState, EmptyState,
GlLink,
GlLoadingIcon, GlLoadingIcon,
}, },
props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
statusPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['installed', 'isLoading', 'hasFunctionData']), ...mapState(['installed', 'isLoading', 'hasFunctionData', 'helpPath', 'statusPath']),
...mapGetters(['getFunctions']), ...mapGetters(['getFunctions']),
checkingInstalled() { checkingInstalled() {
...@@ -118,14 +105,14 @@ export default { ...@@ -118,14 +105,14 @@ export default {
}} }}
</p> </p>
<div class="text-center"> <div class="text-center">
<a :href="helpPath" class="btn btn-success"> <gl-link :href="helpPath" class="btn btn-success">{{
{{ s__('Serverless|Learn more about Serverless') }} s__('Serverless|Learn more about Serverless')
</a> }}</gl-link>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" /> <empty-state v-else />
</section> </section>
</template> </template>
<script> <script>
import { GlDeprecatedButton, GlLink } from '@gitlab/ui'; import { GlDeprecatedButton, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
export default { export default {
...@@ -8,20 +9,13 @@ export default { ...@@ -8,20 +9,13 @@ export default {
GlLink, GlLink,
}, },
props: { props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
missingData: { missingData: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
computed: { computed: {
...mapState(['clustersPath', 'helpPath']),
missingStateClass() { missingStateClass() {
return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state'; return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state';
}, },
......
...@@ -6,6 +6,9 @@ import { createStore } from './store'; ...@@ -6,6 +6,9 @@ import { createStore } from './store';
export default class Serverless { export default class Serverless {
constructor() { constructor() {
if (document.querySelector('.js-serverless-function-details-page') != null) { if (document.querySelector('.js-serverless-function-details-page') != null) {
const entryPointData = document.querySelector('.js-serverless-function-details-page').dataset;
const store = createStore(entryPointData);
const { const {
serviceName, serviceName,
serviceDescription, serviceDescription,
...@@ -15,9 +18,7 @@ export default class Serverless { ...@@ -15,9 +18,7 @@ export default class Serverless {
servicePodcount, servicePodcount,
serviceMetricsUrl, serviceMetricsUrl,
prometheus, prometheus,
clustersPath, } = entryPointData;
helpPath,
} = document.querySelector('.js-serverless-function-details-page').dataset;
const el = document.querySelector('#js-serverless-function-details'); const el = document.querySelector('#js-serverless-function-details');
const service = { const service = {
...@@ -32,35 +33,26 @@ export default class Serverless { ...@@ -32,35 +33,26 @@ export default class Serverless {
this.functionDetails = new Vue({ this.functionDetails = new Vue({
el, el,
store: createStore(), store,
render(createElement) { render(createElement) {
return createElement(FunctionDetails, { return createElement(FunctionDetails, {
props: { props: {
func: service, func: service,
hasPrometheus: prometheus !== undefined, hasPrometheus: prometheus !== undefined,
clustersPath,
helpPath,
}, },
}); });
}, },
}); });
} else { } else {
const { statusPath, clustersPath, helpPath } = document.querySelector( const entryPointData = document.querySelector('.js-serverless-functions-page').dataset;
'.js-serverless-functions-page', const store = createStore(entryPointData);
).dataset;
const el = document.querySelector('#js-serverless-functions'); const el = document.querySelector('#js-serverless-functions');
this.functions = new Vue({ this.functions = new Vue({
el, el,
store: createStore(), store,
render(createElement) { render(createElement) {
return createElement(Functions, { return createElement(Functions);
props: {
clustersPath,
helpPath,
statusPath,
},
});
}, },
}); });
} }
......
...@@ -7,12 +7,12 @@ import createState from './state'; ...@@ -7,12 +7,12 @@ import createState from './state';
Vue.use(Vuex); Vue.use(Vuex);
export const createStore = () => export const createStore = (entryPointData = {}) =>
new Vuex.Store({ new Vuex.Store({
actions, actions,
getters, getters,
mutations, mutations,
state: createState(), state: createState(entryPointData),
}); });
export default createStore(); export default createStore;
export default () => ({ export default (
initialState = { clustersPath: null, helpPath: null, emptyImagePath: null, statusPath: null },
) => ({
clustersPath: initialState.clustersPath,
error: null, error: null,
helpPath: initialState.helpPath,
installed: 'checking', installed: 'checking',
isLoading: true, isLoading: true,
// functions // functions
functions: [], functions: [],
hasFunctionData: true, hasFunctionData: true,
statusPath: initialState.statusPath,
// function_details // function_details
hasPrometheus: true, hasPrometheus: true,
hasPrometheusData: false, hasPrometheusData: false,
graphData: {}, graphData: {},
// empty_state
emptyImagePath: initialState.emptyImagePath,
}); });
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, .serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path,
installed: @installed, installed: @installed,
clusters_path: clusters_path, clusters_path: clusters_path,
help_path: help_page_path('user/project/clusters/serverless/index') } } help_path: help_page_path('user/project/clusters/serverless/index'),
empty_image_path: image_path('illustrations/empty-state/empty-serverless-lg.svg') } }
%div{ class: [('limit-container-width' unless fluid_layout)] } %div{ class: [('limit-container-width' unless fluid_layout)] }
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } } .js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
...@@ -15,5 +16,5 @@ ...@@ -15,5 +16,5 @@
.js-serverless-functions-notice .js-serverless-functions-notice
.flash-container .flash-container
.top-area.adjust.d-flex.justify-content-center .top-area.adjust.d-flex.justify-content-center.gl-border-none
.serverless-functions-table#js-serverless-functions .serverless-functions-table#js-serverless-functions
---
title: Add serverless empty state illustration
merge_request: 36762
author:
type: changed
...@@ -21387,9 +21387,6 @@ msgstr "" ...@@ -21387,9 +21387,6 @@ msgstr ""
msgid "ServerlessURL|Copy URL" msgid "ServerlessURL|Copy URL"
msgstr "" msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr ""
msgid "Serverless|Getting started with serverless" msgid "Serverless|Getting started with serverless"
msgstr "" msgstr ""
...@@ -21399,6 +21396,9 @@ msgstr "" ...@@ -21399,6 +21396,9 @@ msgstr ""
msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available." msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
msgstr "" msgstr ""
msgid "Serverless|In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "Serverless|Install Knative" msgid "Serverless|Install Knative"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyStateComponent should render content 1`] = `
"<section class=\\"row empty-state text-center\\">
<div class=\\"col-12\\">
<div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"Getting started with serverless\\" class=\\"gl-max-w-full\\"></div>
</div>
<div class=\\"col-12\\">
<div class=\\"text-content gl-mx-auto gl-my-0 gl-p-5\\">
<h1 class=\\"h4\\">Getting started with serverless</h1>
<p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
</p>
<div>
<gl-button-stub category=\\"tertiary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub>
<!---->
</div>
</div>
</div>
</section>"
`;
import { createStore } from '~/serverless/store';
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EmptyStateComponent from '~/serverless/components/empty_state.vue';
describe('EmptyStateComponent', () => {
let wrapper;
beforeEach(() => {
const store = createStore({
clustersPath: '/clusters',
helpPath: '/help',
emptyImagePath: '/image.svg',
});
wrapper = shallowMount(EmptyStateComponent, { store, stubs: { GlEmptyState, GlSprintf } });
});
afterEach(() => {
wrapper.destroy();
});
it('should render content', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
...@@ -13,7 +13,7 @@ describe('functionDetailsComponent', () => { ...@@ -13,7 +13,7 @@ describe('functionDetailsComponent', () => {
localVue = createLocalVue(); localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
store = createStore(); store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
}); });
afterEach(() => { afterEach(() => {
...@@ -38,8 +38,6 @@ describe('functionDetailsComponent', () => { ...@@ -38,8 +38,6 @@ describe('functionDetailsComponent', () => {
propsData: { propsData: {
func: serviceStub, func: serviceStub,
hasPrometheus: false, hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
}, },
}); });
...@@ -65,8 +63,6 @@ describe('functionDetailsComponent', () => { ...@@ -65,8 +63,6 @@ describe('functionDetailsComponent', () => {
propsData: { propsData: {
func: serviceStub, func: serviceStub,
hasPrometheus: false, hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
}, },
}); });
...@@ -82,8 +78,6 @@ describe('functionDetailsComponent', () => { ...@@ -82,8 +78,6 @@ describe('functionDetailsComponent', () => {
propsData: { propsData: {
func: serviceStub, func: serviceStub,
hasPrometheus: false, hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
}, },
}); });
...@@ -99,8 +93,6 @@ describe('functionDetailsComponent', () => { ...@@ -99,8 +93,6 @@ describe('functionDetailsComponent', () => {
propsData: { propsData: {
func: serviceStub, func: serviceStub,
hasPrometheus: false, hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
}, },
}); });
......
...@@ -25,55 +25,31 @@ describe('functionsComponent', () => { ...@@ -25,55 +25,31 @@ describe('functionsComponent', () => {
localVue = createLocalVue(); localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
store = createStore(); store = createStore({});
}); });
afterEach(() => { afterEach(() => {
component.vm.$destroy(); component.destroy();
axiosMock.restore(); axiosMock.restore();
}); });
it('should render empty state when Knative is not installed', () => { it('should render empty state when Knative is not installed', () => {
store.dispatch('receiveFunctionsSuccess', { knative_installed: false }); store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, { localVue, store });
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
expect(component.find(EmptyState).exists()).toBe(true); expect(component.find(EmptyState).exists()).toBe(true);
}); });
it('should render a loading component', () => { it('should render a loading component', () => {
store.dispatch('requestFunctionsLoading'); store.dispatch('requestFunctionsLoading');
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, { localVue, store });
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
expect(component.find(GlLoadingIcon).exists()).toBe(true); expect(component.find(GlLoadingIcon).exists()).toBe(true);
}); });
it('should render empty state when there is no function data', () => { it('should render empty state when there is no function data', () => {
store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true }); store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, { localVue, store });
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
expect( expect(
component.vm.$el component.vm.$el
...@@ -91,30 +67,17 @@ describe('functionsComponent', () => { ...@@ -91,30 +67,17 @@ describe('functionsComponent', () => {
...mockServerlessFunctions, ...mockServerlessFunctions,
knative_installed: 'checking', knative_installed: 'checking',
}); });
component = shallowMount(functionsComponent, {
localVue, component = shallowMount(functionsComponent, { localVue, store });
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
expect(component.find('.js-functions-wrapper').exists()).toBe(true); expect(component.find('.js-functions-wrapper').exists()).toBe(true);
expect(component.find('.js-functions-loader').exists()).toBe(true); expect(component.find('.js-functions-loader').exists()).toBe(true);
}); });
it('should render the functions list', () => { it('should render the functions list', () => {
component = shallowMount(functionsComponent, { store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath });
localVue,
store, component = shallowMount(functionsComponent, { localVue, store });
propsData: {
clustersPath: 'clustersPath',
helpPath: 'helpPath',
statusPath,
},
});
component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
......
import { createStore } from '~/serverless/store';
import { GlDeprecatedButton } from '@gitlab/ui'; import { GlDeprecatedButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue'; import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
const createComponent = missingData =>
shallowMount(missingPrometheusComponent, {
propsData: {
clustersPath: '/clusters',
helpPath: '/help',
missingData,
},
});
describe('missingPrometheusComponent', () => { describe('missingPrometheusComponent', () => {
let wrapper; let wrapper;
const createComponent = missingData => {
const store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
wrapper = shallowMount(missingPrometheusComponent, { store, propsData: { missingData } });
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('should render missing prometheus message', () => { it('should render missing prometheus message', () => {
wrapper = createComponent(false); createComponent(false);
const { vm } = wrapper; const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
...@@ -30,7 +28,7 @@ describe('missingPrometheusComponent', () => { ...@@ -30,7 +28,7 @@ describe('missingPrometheusComponent', () => {
}); });
it('should render no prometheus data message', () => { it('should render no prometheus data message', () => {
wrapper = createComponent(true); createComponent(true);
const { vm } = wrapper; const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
......
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