Commit 997552a0 authored by Phil Hughes's avatar Phil Hughes

Merge branch '5276-5-terminal-sync-status-label' into 'master'

[Part 5] Terminal sync service status label in IDE status bar

See merge request gitlab-org/gitlab-ee!14035
parents 26f09226 fea96df5
......@@ -146,7 +146,7 @@ export default {
</div>
<component :is="rightPaneComponent" v-if="currentProjectId" />
</div>
<ide-status-bar :file="activeFile" />
<ide-status-bar />
<new-modal />
</article>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import IdeStatusList from 'ee_else_ce/ide/components/ide_status_list.vue';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
......@@ -12,18 +13,12 @@ export default {
icon,
userAvatarImage,
CiIcon,
IdeStatusList,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
file: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
lastCommitFormatedAge: null,
......@@ -125,11 +120,6 @@ export default {
>{{ lastCommitFormatedAge }}</time
>
</div>
<div v-if="file" class="ide-status-file">{{ file.name }}</div>
<div v-if="file" class="ide-status-file">{{ file.eol }}</div>
<div v-if="file && !file.binary" class="ide-status-file">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div v-if="file" class="ide-status-file">{{ file.fileLanguage }}</div>
<ide-status-list class="ml-auto" />
</footer>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['activeFile']),
},
};
</script>
<template>
<div class="ide-status-list d-flex">
<template v-if="activeFile">
<div class="ide-status-file">{{ activeFile.name }}</div>
<div class="ide-status-file">{{ activeFile.eol }}</div>
<div v-if="!activeFile.binary" class="ide-status-file">
{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}
</div>
<div class="ide-status-file">{{ activeFile.fileLanguage }}</div>
</template>
<slot></slot>
</div>
</template>
......@@ -396,10 +396,6 @@ $ide-commit-header-height: 48px;
font-size: inherit;
}
> div + div {
padding-left: $gl-padding;
}
svg {
vertical-align: sub;
}
......@@ -410,13 +406,14 @@ $ide-commit-header-height: 48px;
}
}
.ide-status-list {
> div + div {
padding-left: $gl-padding;
}
}
.ide-status-file {
text-align: right;
.ide-status-branch + &,
&:first-child {
margin-left: auto;
}
}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
......
<script>
import IdeStatusList from '~/ide/components/ide_status_list.vue';
import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
export default {
components: {
IdeStatusList,
TerminalSyncStatusSafe,
},
};
</script>
<template>
<ide-status-list>
<terminal-sync-status-safe />
</ide-status-list>
</template>
<script>
import _ from 'underscore';
import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
MSG_TERMINAL_SYNC_UPLOADING,
MSG_TERMINAL_SYNC_RUNNING,
} from '../../messages';
export default {
components: {
Icon,
GlLoadingIcon,
},
directives: {
'gl-tooltip': GlTooltipDirective,
},
data() {
return { isLoading: false };
},
computed: {
...mapState('terminalSync', ['isError', 'isStarted', 'message']),
...mapState('terminalSync', {
isLoadingState: 'isLoading',
}),
status() {
if (this.isLoading) {
return {
icon: '',
text: this.isStarted ? MSG_TERMINAL_SYNC_UPLOADING : MSG_TERMINAL_SYNC_CONNECTING,
};
} else if (this.isError) {
return {
icon: 'warning',
text: this.message,
};
} else if (this.isStarted) {
return {
icon: 'mobile-issue-close',
text: MSG_TERMINAL_SYNC_RUNNING,
};
}
return null;
},
},
watch: {
// We want to throttle the `isLoading` updates so that
// the user actually sees an indicator that changes are sent.
isLoadingState: _.throttle(function watchIsLoadingState(val) {
this.isLoading = val;
}, 150),
},
created() {
this.isLoading = this.isLoadingState;
},
};
</script>
<template>
<div
v-if="status"
v-gl-tooltip
:title="status.text"
role="note"
class="d-flex align-items-center"
>
<span>{{ __('Terminal') }}:</span>
<span class="square s16 d-flex-center ml-1" :aria-label="status.text">
<gl-loading-icon v-if="isLoading" inline size="sm" class="d-flex-center" />
<icon v-else-if="status.icon" :name="status.icon" :size="16" />
</span>
</div>
</template>
<script>
import { mapState } from 'vuex';
import TerminalSyncStatus from './terminal_sync_status.vue';
/**
* It is possible that the vuex module is not registered.
*
* This component will gracefully handle this so the actual one can simply use `mapState(moduleName, ...)`.
*/
export default {
components: {
TerminalSyncStatus,
},
computed: {
...mapState(['terminalSync']),
},
};
</script>
<template>
<terminal-sync-status v-if="terminalSync" />
</template>
import { __ } from '~/locale';
export const MSG_TERMINAL_SYNC_CONNECTING = __('Connecting to terminal sync service');
export const MSG_TERMINAL_SYNC_UPLOADING = __('Uploading changes to terminal');
export const MSG_TERMINAL_SYNC_RUNNING = __('Terminal sync service is running');
---
title: Sync file changes from Web IDE to Web Terminal
merge_request: 14035
author:
type: added
import Vuex from 'vuex';
import { createStore } from '~/ide/stores';
import { mount, createLocalVue } from '@vue/test-utils';
import TerminalSyncStatusSafe from 'ee/ide/components/terminal_sync/terminal_sync_status_safe.vue';
import IdeStatusList from 'ee/ide/components/ide_status_list.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ee/ide/components/ide_status_list', () => {
let store;
let wrapper;
const createComponent = () => {
store = createStore();
wrapper = mount(localVue.extend(IdeStatusList), {
localVue,
sync: false,
store,
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders terminal sync status', () => {
createComponent();
expect(wrapper.find(TerminalSyncStatusSafe).exists()).toBe(true);
});
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import TerminalSyncStatus from 'ee/ide/components/terminal_sync/terminal_sync_status.vue';
import TerminalSyncStatusSafe from 'ee/ide/components/terminal_sync/terminal_sync_status_safe.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ee/ide/components/terminal_sync/terminal_sync_status_safe', () => {
let store;
let wrapper;
const createComponent = () => {
store = new Vuex.Store({
state: {},
});
wrapper = shallowMount(localVue.extend(TerminalSyncStatusSafe), {
localVue,
sync: false,
store,
});
};
beforeEach(createComponent);
afterEach(() => {
wrapper.destroy();
});
describe('with terminal sync module in store', () => {
beforeEach(() => {
store.registerModule('terminalSync', {
state: {},
});
});
it('renders terminal sync status', () => {
expect(wrapper.find(TerminalSyncStatus).exists()).toBe(true);
});
});
describe('without terminal sync module', () => {
it('does not render terminal sync status', () => {
expect(wrapper.find(TerminalSyncStatus).exists()).toBe(false);
});
});
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TerminalSyncStatus from 'ee/ide/components/terminal_sync/terminal_sync_status.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
MSG_TERMINAL_SYNC_UPLOADING,
MSG_TERMINAL_SYNC_RUNNING,
} from 'ee/ide/messages';
const TEST_MESSAGE = 'lorem ipsum dolar sit';
const START_LOADING = 'START_LOADING';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ee/ide/components/terminal_sync/terminal_sync_status', () => {
let moduleState;
let store;
let wrapper;
const createComponent = () => {
store = new Vuex.Store({
modules: {
terminalSync: {
namespaced: true,
state: moduleState,
mutations: {
[START_LOADING]: state => {
state.isLoading = true;
},
},
},
},
});
wrapper = shallowMount(localVue.extend(TerminalSyncStatus), {
localVue,
sync: false,
store,
});
};
beforeEach(() => {
moduleState = {
isLoading: false,
isStarted: false,
isError: false,
message: '',
};
});
afterEach(() => {
wrapper.destroy();
});
describe('when doing nothing', () => {
it('shows nothing', () => {
createComponent();
expect(wrapper.isEmpty()).toBe(true);
});
});
describe.each`
description | state | statusMessage | icon
${'when loading'} | ${{ isLoading: true }} | ${MSG_TERMINAL_SYNC_CONNECTING} | ${''}
${'when loading and started'} | ${{ isLoading: true, isStarted: true }} | ${MSG_TERMINAL_SYNC_UPLOADING} | ${''}
${'when error'} | ${{ isError: true, message: TEST_MESSAGE }} | ${TEST_MESSAGE} | ${'warning'}
${'when started'} | ${{ isStarted: true }} | ${MSG_TERMINAL_SYNC_RUNNING} | ${'mobile-issue-close'}
`('$description', ({ state, statusMessage, icon }) => {
beforeEach(() => {
Object.assign(moduleState, state);
createComponent();
});
it('shows message', () => {
expect(wrapper.attributes('data-original-title')).toContain(statusMessage);
});
if (!icon) {
it('does not render icon', () => {
expect(wrapper.find(Icon).exists()).toBe(false);
});
it('renders loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
} else {
it('renders icon', () => {
expect(wrapper.find(Icon).props('name')).toEqual(icon);
});
it('does not render loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
}
});
});
......@@ -3454,6 +3454,9 @@ msgstr ""
msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
msgstr ""
msgid "Connecting to terminal sync service"
msgstr ""
msgid "Connecting..."
msgstr ""
......@@ -12939,6 +12942,9 @@ msgstr ""
msgid "Terminal for environment"
msgstr ""
msgid "Terminal sync service is running"
msgstr ""
msgid "Terms of Service Agreement and Privacy Policy"
msgstr ""
......@@ -14382,6 +14388,9 @@ msgstr ""
msgid "Uploaded on"
msgstr ""
msgid "Uploading changes to terminal"
msgstr ""
msgid "Uploads"
msgstr ""
......
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import IdeStatusList from '~/ide/components/ide_status_list';
const TEST_FILE = {
name: 'lorem.md',
eol: 'LF',
editorRow: 3,
editorColumn: 23,
fileLanguage: 'markdown',
};
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ide/components/ide_status_list', () => {
let activeFile;
let store;
let wrapper;
const createComponent = (options = {}) => {
store = new Vuex.Store({
getters: {
activeFile: () => activeFile,
},
});
wrapper = shallowMount(localVue.extend(IdeStatusList), {
localVue,
sync: false,
store,
...options,
});
};
beforeEach(() => {
activeFile = TEST_FILE;
});
afterEach(() => {
wrapper.destroy();
store = null;
wrapper = null;
});
const getEditorPosition = file => `${file.editorRow}:${file.editorColumn}`;
describe('with regular file', () => {
beforeEach(() => {
createComponent();
});
it('shows file name', () => {
expect(wrapper.text()).toContain(TEST_FILE.name);
});
it('shows file eol', () => {
expect(wrapper.text()).toContain(TEST_FILE.name);
});
it('shows file editor position', () => {
expect(wrapper.text()).toContain(getEditorPosition(TEST_FILE));
});
it('shows file language', () => {
expect(wrapper.text()).toContain(TEST_FILE.fileLanguage);
});
});
describe('with binary file', () => {
beforeEach(() => {
activeFile.binary = true;
createComponent();
});
it('does not show file editor position', () => {
expect(wrapper.text()).not.toContain(getEditorPosition(TEST_FILE));
});
});
it('adds slot as child of list', () => {
createComponent({
slots: {
default: ['<div class="js-test">Hello</div>', '<div class="js-test">World</div>'],
},
});
expect(wrapper.find('.ide-status-list').findAll('.js-test').length).toEqual(2);
});
});
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