Skip to content
代码片段 群组 项目
提交 313bc256 编辑于 作者: Paul Slaughter's avatar Paul Slaughter
浏览文件

Merge branch '391543-remote-dev-empty-state' into 'master'

No related branches found
No related tags found
无相关合并请求
显示
363 个添加0 个删除
......@@ -163,6 +163,7 @@
draw :jira_connect
Gitlab.ee do
draw :remote_development
draw :security
draw :smartcard
draw :trial
......
---
stage: Create
group: Editor
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
> Introduced in GitLab 15.11 [with a flag](../../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `remote_development_feature_flag`.
On GitLab.com, this feature is not available.
The feature is not ready for production use.
# Tutorial: Create and run your first GitLab Workspace **(ULTIMATE)**
This tutorial shows you how to configure and run your first Remote Development Workspace in GitLab.
import { initWorkspacesApp } from 'ee/remote_development/init_workspaces_app';
initWorkspacesApp();
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const i18n = {
title: s__('Workspaces|Develop anywhere'),
description: s__(`Workspaces|GitLab Workspaces is a powerful collaborative platform that provides a
comprehensive set of tools for software development
teams to manage their entire development lifecycle.`),
primaryButtonText: s__('Workspaces|Get started with GitLab Workspaces'),
};
export default {
components: {
GlEmptyState,
},
inject: ['emptyStateSvgPath'],
computed: {
workspaceHelpPagePath() {
return helpPagePath('user/workspace/quick_start/index.md');
},
},
i18n,
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.title"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.primaryButtonText"
:primary-button-link="workspaceHelpPagePath"
:svg-path="emptyStateSvgPath"
/>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import App from './pages/app.vue';
import createRouter from './router';
Vue.use(VueApollo);
const apolloClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
const initWorkspacesApp = () => {
const el = document.querySelector('#js-workspaces');
if (!el) {
return null;
}
const { workspacesListPath, emptyStateSvgPath } = el.dataset;
const router = createRouter({
base: workspacesListPath,
});
return new Vue({
el,
name: 'WorkspacesRoot',
router,
apolloProvider,
provide: {
workspacesListPath,
emptyStateSvgPath,
},
render: (createElement) => createElement(App),
});
};
export { initWorkspacesApp };
<template>
<div>
<router-view />
</div>
</template>
<script>
import EmptyState from '../components/list/empty_state.vue';
export default {
components: {
EmptyState,
},
};
</script>
<template>
<div>
<empty-state />
</div>
</template>
import Vue from 'vue';
import VueRouter from 'vue-router';
import WorkspacesList from '../pages/list.vue';
Vue.use(VueRouter);
export default function createRouter({ base }) {
const routes = [
{
path: '/',
name: 'index',
component: WorkspacesList,
},
{
path: '*',
redirect: '/',
},
];
return new VueRouter({
base,
mode: 'history',
routes,
});
}
# frozen_string_literal: true
module RemoteDevelopment
class WorkspacesController < ApplicationController
before_action :authorize_remote_development!, only: [:index]
feature_category :remote_development
urgency :low
def index; end
private
def authorize_remote_development!
render_404 unless can?(current_user, :read_workspace)
end
end
end
......@@ -151,6 +151,7 @@ class Features
protected_environments
reject_non_dco_commits
reject_unsigned_commits
remote_development
saml_group_sync
service_accounts
scoped_labels
......
......@@ -9,6 +9,10 @@ module GlobalPolicy
License.feature_available?(:operations_dashboard)
end
condition(:remote_development_available) do
::Feature.enabled?(:remote_development_feature_flag) && License.feature_available?(:remote_development)
end
condition(:pages_size_limit_available) do
License.feature_available?(:pages_size_limit)
end
......@@ -43,6 +47,10 @@ module GlobalPolicy
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
rule { ~anonymous & remote_development_available }.policy do
enable :read_workspace
end
rule { admin & instance_devops_adoption_available }.policy do
enable :manage_devops_adoption_namespaces
enable :view_instance_devops_adoption
......
- page_title s_('Workspaces')
- nav 'your_work'
- @left_sidebar = true
#js-workspaces{ data: {
workspaces_list_path: remote_development_workspaces_path,
empty_state_svg_path: image_path('illustrations/empty-state/empty-workspaces-md.svg')
}
}
---
name: remote_development_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112397
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391543
milestone: '15.11'
type: development
group: group::editor
default_enabled: false
# frozen_string_literal: true
namespace :remote_development do
resources :workspaces, path: 'workspaces(/*vueroute)'
end
......@@ -10,6 +10,7 @@ module Panel
def configure_menus
super
add_menu(workspaces_menu)
add_menu(environments_dashboard_menu)
add_menu(operations_dashboard_menu)
add_menu(security_dashboard_menu)
......@@ -19,6 +20,10 @@ def configure_menus
private
def workspaces_menu
::Sidebars::YourWork::Menus::WorkspacesMenu.new(context)
end
def environments_dashboard_menu
::Sidebars::YourWork::Menus::EnvironmentsDashboardMenu.new(context)
end
......
# frozen_string_literal: true
module Sidebars
module YourWork
module Menus
class WorkspacesMenu < ::Sidebars::Menu
override :link
def link
remote_development_workspaces_path
end
override :title
def title
_('Workspaces')
end
override :sprite_icon
def sprite_icon
'cloud-gear'
end
override :render?
def render?
can?(context.current_user, :read_workspace)
end
override :active_routes
def active_routes
{ path: 'remote_development/workspaces#index' }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RemoteDevelopment::WorkspacesController, feature_category: :remote_development do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
shared_examples 'remote development feature flag' do |feature_flag_enabled, expected_status|
before do
stub_licensed_features(remote_development: true)
stub_feature_flags(remote_development_feature_flag: feature_flag_enabled)
end
describe 'GET #index' do
it 'responds with the expected status' do
get :index
expect(response).to have_gitlab_http_status(expected_status)
end
end
end
context 'with remote development feature flag' do
it_behaves_like 'remote development feature flag', true, :ok
it_behaves_like 'remote development feature flag', false, :not_found
end
context 'with remote development not licensed' do
before do
stub_licensed_features(remote_development: false)
end
describe 'GET #index' do
it 'responds with the not found status' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import EmptyState, { i18n } from 'ee/remote_development/components/list/empty_state.vue';
const QUICK_START_LINK = '/user/workspace/quick_start/index.md';
const SVG_PATH = '/assets/illustrations/empty_states/empty_workspaces.svg';
describe('remote_development/components/list/empty_state.vue', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = () => {
wrapper = shallowMount(EmptyState, {
provide: {
emptyStateSvgPath: SVG_PATH,
},
});
};
describe('when no workspaces exist', () => {
it('should render empty workspace state', () => {
createComponent();
expect(findEmptyState().props()).toMatchObject({
title: i18n.title,
description: i18n.description,
primaryButtonText: i18n.primaryButtonText,
primaryButtonLink: helpPagePath(QUICK_START_LINK),
svgPath: SVG_PATH,
});
});
});
});
import { createWrapper } from '@vue/test-utils';
import { initWorkspacesApp } from 'ee/remote_development/init_workspaces_app';
import EmptyState from 'ee/remote_development/components/list/empty_state.vue';
describe('ee/remote_development/init_workspaces_app', () => {
let wrapper;
beforeEach(() => {
document.body.innerHTML = '<div id="js-workspaces"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('initWorkspacesApp - integration', () => {
beforeEach(() => {
wrapper = createWrapper(initWorkspacesApp());
});
it('renders empty state', () => {
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
});
describe('initWorkspacesApp - when mounting element not found', () => {
it('returns null', () => {
document.body.innerHTML = '<div>Look ma! Code!</div>';
expect(initWorkspacesApp()).toBeNull();
});
});
});
import { shallowMount } from '@vue/test-utils';
import WorkspacesList from 'ee/remote_development/pages/list.vue';
import EmptyState from 'ee/remote_development/components/list/empty_state.vue';
describe('remote_development/pages/list.vue', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(EmptyState);
const createComponent = () => {
wrapper = shallowMount(WorkspacesList, {});
};
describe('when no workspaces exist', () => {
it('should render empty workspace state', () => {
createComponent();
expect(findEmptyState().exists()).toBe(true);
});
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册