Skip to content
代码片段 群组 项目
提交 29e13db9 编辑于 作者: Robert Hunt's avatar Robert Hunt 提交者: Jannik Lehmann
浏览文件

Add creating instance to onboarding view

- Added creating instance content
- Added temporary data attribute to test the content works
- Updated specs
- Updated translations
上级 f807e2f5
No related branches found
No related tags found
无相关合并请求
显示
456 个添加44 个删除
query getProjectJitsuKey($projectPath: ID!) {
project(fullPath: $projectPath) {
id
jitsuKey
}
}
mutation initializeProductAnalytics($projectPath: ID!) {
projectInitializeProductAnalytics(input: { projectPath: $projectPath }) {
project {
id
fullPath
}
errors
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AnalyticsApp from './product_analytics_app.vue';
import createRouter from './router';
......@@ -12,16 +14,24 @@ export default () => {
const {
jitsuKey,
projectId,
projectFullPath,
jitsuHost,
jitsuProjectId,
chartEmptyStateIllustrationPath,
} = el.dataset;
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
router: createRouter(),
provide: {
jitsuKey,
projectFullPath,
projectId,
jitsuHost,
jitsuProjectId,
......
<script>
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { EMPTY_STATE_I18N } from '../constants';
export default {
name: 'AnalyticsEmptyState',
components: {
GlButton,
GlEmptyState,
GlLoadingIcon,
},
inject: {
chartEmptyStateIllustrationPath: {
type: String,
},
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
title() {
return this.loading ? EMPTY_STATE_I18N.loading.title : EMPTY_STATE_I18N.empty.title;
},
description() {
return this.loading
? EMPTY_STATE_I18N.loading.description
: EMPTY_STATE_I18N.empty.description;
},
},
i18n: EMPTY_STATE_I18N,
docsPath: helpPagePath('user/product_analytics/index'),
};
</script>
<template>
<gl-empty-state :title="title" :svg-path="chartEmptyStateIllustrationPath">
<template #description>
<p class="gl-max-w-80">
{{ description }}
</p>
</template>
<template #actions>
<template v-if="!loading">
<gl-button variant="confirm" data-testid="setup-btn" @click="$emit('initialize')">
{{ $options.i18n.empty.setUpBtnText }}
</gl-button>
<gl-button :href="$options.docsPath" data-testid="learn-more-btn">
{{ $options.i18n.empty.learnMoreBtnText }}
</gl-button>
</template>
<gl-loading-icon v-else size="lg" class="gl-mt-5" />
</template>
</gl-empty-state>
</template>
import { s__, __ } from '~/locale';
export const INSTALL_NPM_PACKAGE = `yarn add @gitlab/application-sdk-js
--
......@@ -17,3 +19,22 @@ export const HTML_SCRIPT_SETUP = `<script src="https://unpkg.com/@gitlab/applica
applicationId: '$applicationId',
host: '$host',
});</script>`;
export const JITSU_KEY_CHECK_DELAY = 1000;
export const EMPTY_STATE_I18N = {
empty: {
title: s__('Product Analytics|Analyze your product with Product Analytics'),
description: s__(
'Product Analytics|Set up Product Analytics to track how your product is performing. Combine it with your GitLab data to better understand where you can improve your product and development processes.',
),
setUpBtnText: s__('Product Analytics|Set up product analytics'),
learnMoreBtnText: __('Learn more'),
},
loading: {
title: s__('Product Analytics|Creating your product analytics instance...'),
description: s__(
'Product Analytics|This might take a while, feel free to navigate away from this page and come back later.',
),
},
};
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { createAlert } from '~/flash';
import initializeProductAnalyticsMutation from '../graphql/mutations/initialize_product_analytics.mutation.graphql';
import getProjectJitsuKeyQuery from '../graphql/mutations/get_project_jitsu_key.query.graphql';
import OnboardingEmptyState from './components/onboarding_empty_state.vue';
import { JITSU_KEY_CHECK_DELAY } from './constants';
export default {
name: 'ProductAnalyticsOnboardingView',
components: {
GlEmptyState,
OnboardingEmptyState,
},
inject: {
chartEmptyStateIllustrationPath: {
projectFullPath: {
type: String,
},
},
i18n: {
title: s__('Product Analytics|Analyze your product with Product Analytics'),
description: s__(
'Product Analytics|Set up Product Analytics to track how your product is performing. Combine it with your GitLab data to better understand where you can improve your product and development processes.',
),
setUpBtnText: s__('Product Analytics|Set up product analytics...'),
learnMoreBtnText: __('Learn more'),
data() {
return {
creatingInstance: false,
jitsuKey: null,
pollJitsuKey: false,
};
},
apollo: {
jitsuKey: {
query: getProjectJitsuKeyQuery,
variables() {
return {
projectPath: this.projectFullPath,
};
},
pollInterval: JITSU_KEY_CHECK_DELAY,
update({ project }) {
const { jitsuKey } = project || {};
this.pollJitsuKey = !jitsuKey;
return jitsuKey;
},
skip() {
return !this.pollJitsuKey;
},
error(err) {
this.showError(err);
this.pollJitsuKey = false;
},
},
},
computed: {
loading() {
return this.creatingInstance || this.pollJitsuKey;
},
},
methods: {
showError(error, captureError = true) {
createAlert({
message: error.message,
captureError,
error,
});
},
async initializeProductAnalytics() {
this.creatingInstance = true;
try {
const { data } = await this.$apollo.mutate({
mutation: initializeProductAnalyticsMutation,
variables: {
projectPath: this.projectFullPath,
},
context: {
isSingleRequest: true,
},
});
const [error] = data?.projectInitializeProductAnalytics?.errors || [];
if (error) {
this.showError(new Error(error), false);
} else {
this.pollJitsuKey = true;
}
} catch (err) {
// TODO: Update to show the tracking codes view when no error in https://gitlab.com/gitlab-org/gitlab/-/issues/381320
this.showError(err);
} finally {
this.creatingInstance = false;
}
},
},
docsPath: helpPagePath('user/product_analytics/index'),
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="chartEmptyStateIllustrationPath"
:primary-button-text="$options.i18n.setUpBtnText"
primary-button-link="#"
:secondary-button-text="$options.i18n.learnMoreBtnText"
:secondary-button-link="$options.docsPath"
>
<template #description>
<p class="gl-max-w-80">
{{ $options.i18n.description }}
</p>
</template>
</gl-empty-state>
<onboarding-empty-state :loading="loading" @initialize="initializeProductAnalytics" />
</template>
......@@ -7,6 +7,7 @@
jitsu_host: Gitlab::CurrentSettings.current_application_settings.jitsu_host,
jitsu_project_id: Gitlab::CurrentSettings.current_application_settings.jitsu_project_xid,
chart_empty_state_illustration_path: image_path('illustrations/chart-empty-state.svg'),
project_full_path: @project.full_path,
}
}
= gl_loading_icon(size: 'lg', css_class: 'gl-my-7')
......@@ -30,7 +30,7 @@
end
end
shared_examples 'renders the onboarding view' do
shared_examples 'renders the onboarding empty state' do
before do
visit_page
end
......@@ -93,7 +93,38 @@
end
context 'without the Jitsu key' do
it_behaves_like 'renders the onboarding view'
it_behaves_like 'renders the onboarding empty state'
context 'when setting up a new instance' do
before do
visit_page
click_button s_('Product Analytics|Set up product analytics')
end
it 'renders the creating instance loading screen' do
expect(page).to have_content(s_('Product Analytics|Creating your product analytics instance...'))
end
# TODO: Update to show the tracking codes view when no error in https://gitlab.com/gitlab-org/gitlab/-/issues/381320
it 'returns back to the onboarding empty state after creating the new instance' do
wait_for_requests
expect(page).to have_content(s_('Product Analytics|Analyze your product with Product Analytics'))
end
context 'when a new instance has already been initialized' do
before do
visit_page
end
it 'renders an error alert when setting up a new instance' do
click_button s_('Product Analytics|Set up product analytics')
expect(find('[data-testid="alert-danger"]'))
.to have_text(/Product analytics initialization is already (completed|in progress)/)
end
end
end
end
context 'with the Jitsu key' do
......
export const createInstanceResponse = (errors = []) => ({
data: {
projectInitializeProductAnalytics: {
project: {
id: 'gid://gitlab/Project/2',
fullPath: '',
},
errors,
},
},
});
export const getJitsuKeyResponse = (jitsuKey = null) => ({
data: {
project: {
id: 'gid://gitlab/Project/2',
jitsuKey,
},
},
});
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import OnboardingEmptyState from 'ee/product_analytics/onboarding/components/onboarding_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'spec/test_constants';
import { EMPTY_STATE_I18N } from 'ee/product_analytics/onboarding/constants';
describe('OnboardingEmptyState', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSetupBtn = () => wrapper.findByTestId('setup-btn');
const findLearnMoreBtn = () => wrapper.findByTestId('learn-more-btn');
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(OnboardingEmptyState, {
provide: {
chartEmptyStateIllustrationPath: TEST_HOST,
},
propsData: {
...props,
},
});
};
describe('default behaviour', () => {
beforeEach(() => {
createWrapper();
});
it('should render the empty state with expected props', () => {
const emptyState = findEmptyState();
expect(emptyState.props()).toMatchObject({
title: EMPTY_STATE_I18N.empty.title,
svgPath: TEST_HOST,
});
expect(emptyState.text()).toContain(EMPTY_STATE_I18N.empty.description);
expect(findSetupBtn().text()).toBe(EMPTY_STATE_I18N.empty.setUpBtnText);
expect(findLearnMoreBtn().text()).toBe(EMPTY_STATE_I18N.empty.learnMoreBtnText);
expect(findLearnMoreBtn().attributes('href')).toBe('/help/user/product_analytics/index');
});
it('should emit `initialize` when the setup button is clicked', () => {
findSetupBtn().vm.$emit('click');
expect(wrapper.emitted('initialize')).toStrictEqual([[]]);
});
it('does not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('when loading', () => {
beforeEach(() => {
createWrapper({ loading: true });
});
it('should render the loading state', () => {
const emptyState = findEmptyState();
expect(emptyState.props()).toMatchObject({
title: EMPTY_STATE_I18N.loading.title,
svgPath: TEST_HOST,
});
expect(emptyState.text()).toContain(EMPTY_STATE_I18N.loading.description);
});
it('renders the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render the buttons', () => {
expect(findSetupBtn().exists()).toBe(false);
expect(findLearnMoreBtn().exists()).toBe(false);
});
});
});
import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import ProductAnalyticsOnboardingView from 'ee/product_analytics/onboarding/onboarding_view.vue';
import OnboardingEmptyState from 'ee/product_analytics/onboarding/components/onboarding_empty_state.vue';
import initializeProductAnalyticsMutation from 'ee/product_analytics/graphql/mutations/initialize_product_analytics.mutation.graphql';
import getProjectJitsuKeyQuery from 'ee/product_analytics/graphql/mutations/get_project_jitsu_key.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { JITSU_KEY_CHECK_DELAY } from 'ee/product_analytics/onboarding/constants';
import { createInstanceResponse, getJitsuKeyResponse } from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
describe('ProductAnalyticsOnboardingView', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const fatalError = new Error('GraphQL networkError');
const apiErrorMsg = 'Product analytics initialization is already complete';
const jitsuKey = 'valid-jitsu-key';
const mockCreateInstanceSuccess = jest.fn().mockResolvedValue(createInstanceResponse());
const mockCreateInstanceLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
const mockCreateInstanceApiError = jest
.fn()
.mockResolvedValue(createInstanceResponse([apiErrorMsg]));
const mockCreateInstanceFatalError = jest.fn().mockRejectedValue(fatalError);
const mockGetJitsuKeyHasKeySuccess = jest.fn().mockResolvedValue(getJitsuKeyResponse(jitsuKey));
const mockGetJitsuKeyHasKeySuccessRetry = jest
.fn()
.mockResolvedValueOnce(getJitsuKeyResponse(null))
.mockResolvedValueOnce(getJitsuKeyResponse(jitsuKey));
const mockGetJitsuKeyError = jest.fn().mockRejectedValue(fatalError);
const findEmptyState = () => wrapper.findComponent(OnboardingEmptyState);
const createWrapper = () => {
const createWrapper = (handlers) => {
wrapper = shallowMountExtended(ProductAnalyticsOnboardingView, {
apolloProvider: createMockApollo(handlers),
provide: {
chartEmptyStateIllustrationPath: TEST_HOST,
projectFullPath: 'group-1/project-1',
},
});
};
const waitForApolloTimers = async () => {
jest.advanceTimersByTime(JITSU_KEY_CHECK_DELAY);
return waitForPromises();
};
afterEach(() => {
createAlert.mockClear();
});
describe('when mounted', () => {
beforeEach(() => {
createWrapper();
});
it('should render the empty state with expected props', () => {
const emptyState = findEmptyState();
it('should render the empty state that is not loading', () => {
expect(findEmptyState().props('loading')).toBe(false);
});
it('has a polling interval for querying the jitsu key', () => {
expect(wrapper.vm.$apollo.queries.jitsuKey.options.pollInterval).toBe(JITSU_KEY_CHECK_DELAY);
});
});
describe('when creating an instance', () => {
it('should show loading while the instance is initializing', async () => {
createWrapper([[initializeProductAnalyticsMutation, mockCreateInstanceLoading]]);
await findEmptyState().vm.$emit('initialize');
expect(findEmptyState().props('loading')).toBe(true);
});
it('should show loading and poll for the jitsu key while it is null', async () => {
createWrapper([
[initializeProductAnalyticsMutation, mockCreateInstanceSuccess],
[getProjectJitsuKeyQuery, mockGetJitsuKeyHasKeySuccessRetry],
]);
findEmptyState().vm.$emit('initialize');
await waitForPromises();
expect(mockGetJitsuKeyHasKeySuccessRetry.mock.calls).toHaveLength(1);
expect(findEmptyState().props('loading')).toBe(true);
await waitForApolloTimers();
expect(mockGetJitsuKeyHasKeySuccessRetry.mock.calls).toHaveLength(2);
expect(findEmptyState().props('loading')).toBe(false);
});
it('should return the jitsu key if creating an instance is successful', async () => {
createWrapper([
[initializeProductAnalyticsMutation, mockCreateInstanceSuccess],
[getProjectJitsuKeyQuery, mockGetJitsuKeyHasKeySuccess],
]);
findEmptyState().vm.$emit('initialize');
expect(emptyState.props()).toMatchObject({
title: ProductAnalyticsOnboardingView.i18n.title,
svgPath: TEST_HOST,
primaryButtonText: ProductAnalyticsOnboardingView.i18n.setUpBtnText,
primaryButtonLink: '#',
secondaryButtonText: ProductAnalyticsOnboardingView.i18n.learnMoreBtnText,
secondaryButtonLink: ProductAnalyticsOnboardingView.docsPath,
await waitForPromises();
expect(mockGetJitsuKeyHasKeySuccess).toHaveBeenCalledTimes(1);
expect(findEmptyState().props('loading')).toBe(false);
});
it('should show the error if getting the jitsu key throws an error', async () => {
createWrapper([
[initializeProductAnalyticsMutation, mockCreateInstanceSuccess],
[getProjectJitsuKeyQuery, mockGetJitsuKeyError],
]);
findEmptyState().vm.$emit('initialize');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: fatalError.message,
captureError: true,
error: fatalError,
});
expect(emptyState.text()).toContain(ProductAnalyticsOnboardingView.i18n.description);
});
describe('when a create instance error occurs', () => {
it.each`
type | mockError | alertError | captureError
${'instance'} | ${mockCreateInstanceFatalError} | ${fatalError} | ${true}
${'api'} | ${mockCreateInstanceApiError} | ${new Error(apiErrorMsg)} | ${false}
`(
'should create an alert for $type errors',
async ({ mockError, alertError, captureError }) => {
createWrapper([[initializeProductAnalyticsMutation, mockError]]);
findEmptyState().vm.$emit('initialize');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: alertError.message,
captureError,
error: alertError,
});
},
);
});
});
});
......@@ -28,11 +28,12 @@ describe('ProductAnalyticsApp', () => {
wrapper = shallowMount(AnalyticsApp, {
router: createRouter(),
provide: {
chartEmptyStateIllustrationPath: TEST_HOST,
jitsuKey: '123',
projectId: '1',
jitsuHost: TEST_HOST,
jitsuProjectId: '',
chartEmptyStateIllustrationPath: TEST_HOST,
projectFullPath: 'group-1/project-1',
...provided,
},
});
......
......@@ -31810,6 +31810,9 @@ msgstr ""
msgid "Product Analytics|Analyze your product with Product Analytics"
msgstr ""
 
msgid "Product Analytics|Creating your product analytics instance..."
msgstr ""
msgid "Product Analytics|For the product analytics dashboard to start showing you some data, you need to add the analytics tracking code to your project."
msgstr ""
 
......@@ -31831,7 +31834,7 @@ msgstr ""
msgid "Product Analytics|Set up Product Analytics to track how your product is performing. Combine it with your GitLab data to better understand where you can improve your product and development processes."
msgstr ""
 
msgid "Product Analytics|Set up product analytics..."
msgid "Product Analytics|Set up product analytics"
msgstr ""
 
msgid "Product Analytics|Steps to add product analytics as a CommonJS module"
......@@ -31846,6 +31849,9 @@ msgstr ""
msgid "Product Analytics|The host to send all tracking events to"
msgstr ""
 
msgid "Product Analytics|This might take a while, feel free to navigate away from this page and come back later."
msgstr ""
msgid "Product Analytics|To instrument your application, select one of the options below. After an option has been instrumented and data is being collected, this page will progress to the next step."
msgstr ""
 
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册