Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add workspaces default editor and components #567

Merged
merged 7 commits into from
Jun 27, 2022
26 changes: 16 additions & 10 deletions packages/dashboard-backend/src/api/serverConfigApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ import { getSchema } from '../services/helpers';
const tags = ['Server Config'];

export function registerServerConfigApi(server: FastifyInstance) {
server.get(
`${baseApiPath}/server-config/default-plugins`,
getSchema({ tags }),
async function () {
const token = await getServiceAccountToken();
const { serverConfigApi } = await getDevWorkspaceClient(token);
const cheCustomResource = await serverConfigApi.getCheCustomResource();
server.get(`${baseApiPath}/server-config`, getSchema({ tags }), async function () {
const token = await getServiceAccountToken();
const { serverConfigApi } = await getDevWorkspaceClient(token);
const cheCustomResource = await serverConfigApi.getCheCustomResource();

return serverConfigApi.getDefaultPlugins(cheCustomResource);
},
);
const plugins = serverConfigApi.getDefaultPlugins(cheCustomResource);
const editor = serverConfigApi.getDefaultEditor(cheCustomResource);
const components = serverConfigApi.getDefaultComponents(cheCustomResource);

return {
defaults: {
editor,
plugins,
components,
},
};
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as k8s from '@kubernetes/client-node';
import { IServerConfigApi } from '../../types';
import { createError } from '../helpers';
import { api } from '@eclipse-che/common';
import { V220DevfileComponents } from '@devfile/api';

const CUSTOM_RESOURCE_DEFINITIONS_API_ERROR_LABEL = 'CUSTOM_RESOURCE_DEFINITIONS_API_ERROR';

Expand Down Expand Up @@ -61,6 +62,14 @@ export class ServerConfigApi implements IServerConfigApi {
return cheCustomResource.spec.server.workspacesDefaultPlugins || [];
}

getDefaultEditor(cheCustomResource: { [key: string]: any }): string | undefined {
return cheCustomResource.spec.server.workspaceDefaultEditor;
}

getDefaultComponents(cheCustomResource: { [key: string]: any }): V220DevfileComponents[] {
return cheCustomResource.spec.server.workspaceDefaultComponents || [];
}

getDashboardWarning(cheCustomResource: { [key: string]: any }): string | undefined {
return cheCustomResource.spec.dashboard?.warning;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import { V1alpha2DevWorkspace, V1alpha2DevWorkspaceTemplate } from '@devfile/api';
import { api } from '@eclipse-che/common';
import { V220DevfileComponents } from '@devfile/api';

/**
* Holds the methods for working with dockerconfig for devworkspace
Expand Down Expand Up @@ -94,7 +95,15 @@ export interface IServerConfigApi {
* Returns default plugins
*/
getDefaultPlugins(cheCustomResource: { [key: string]: any }): api.IWorkspacesDefaultPlugins[];

/**
* Returns the default editor to workspace create with. It could be a plugin ID or a URI.
*/
getDefaultEditor(cheCustomResource: { [key: string]: any }): string | undefined;
/**
* Returns the default components applied to DevWorkspaces.
* These default components are meant to be used when a Devfile does not contain any components.
*/
getDefaultComponents(cheCustomResource: { [key: string]: any }): V220DevfileComponents[];
/**
* Returns a maintenance warning
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class SamplesListGallery extends React.PureComponent<Props, State> {
: `&storageType=${storageType}`;
}
// use factory workflow to load the getting started samples
const factoryUrl = `${window.location.origin}/#/load-factory?url=${link}${devWorkspace}`;
const factoryUrl = `${window.location.origin}/dashboard/#/load-factory?url=${link}${devWorkspace}`;
// open a new page to handle that
window.open(factoryUrl, '_blank');
this.isLoading = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('Samples List Gallery', () => {
fireEvent.click(cardHeader);
jest.runOnlyPendingTimers();
expect(windowSpy).toBeCalledWith(
'http://localhost/#/load-factory?url=http%3A%2F%2Fmy-fake-repository.com%2F',
'http://localhost/dashboard/#/load-factory?url=http%3A%2F%2Fmy-fake-repository.com%2F',
'_blank',
);
});
Expand Down
12 changes: 11 additions & 1 deletion packages/dashboard-frontend/src/services/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as BannerAlertStore from '../../store/BannerAlert';
import * as BrandingStore from '../../store/Branding';
import * as ClusterConfigStore from '../../store/ClusterConfig';
import * as ClusterInfoStore from '../../store/ClusterInfo';
import * as ServerConfigStore from '../../store/ServerConfig';
import * as DevfileRegistriesStore from '../../store/DevfileRegistries';
import * as InfrastructureNamespacesStore from '../../store/InfrastructureNamespaces';
import * as PluginsStore from '../../store/Plugins/chePlugins';
Expand Down Expand Up @@ -77,9 +78,9 @@ export default class Bootstrap {
const results = await Promise.allSettled([
this.fetchCurrentUser(),
this.fetchUserProfile(),
this.fetchServerConfig().then(() => this.fetchDefaultDwPlugins(settings)),
this.fetchPlugins(settings).then(() => this.fetchDevfileSchema()),
this.fetchDwPlugins(settings),
this.fetchDefaultDwPlugins(settings),
this.fetchRegistriesMetadata(settings),
this.watchNamespaces(),
this.updateDevWorkspaceTemplates(settings),
Expand Down Expand Up @@ -253,6 +254,15 @@ export default class Bootstrap {
return this.store.getState().workspacesSettings.settings;
}

private async fetchServerConfig(): Promise<void> {
const { requestServerConfig } = ServerConfigStore.actionCreators;
try {
await requestServerConfig()(this.store.dispatch, this.store.getState, undefined);
} catch (e) {
console.warn('Unable to fetch server config.');
}
}

private async fetchRegistriesMetadata(settings: che.WorkspaceSettings): Promise<void> {
const { requestRegistriesMetadata } = DevfileRegistriesStore.actionCreators;
await requestRegistriesMetadata(settings.cheWorkspaceDevfileRegistryUrl || '')(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@ import axios from 'axios';
import common from '@eclipse-che/common';
import { prefix } from './const';
import { api } from '@eclipse-che/common';
import { V220DevfileComponents } from '@devfile/api';

/**
* Returns an array of default plug-ins per editor
*
* @returns Promise resolving with the array of default plug-ins for the specified editor
* @returns Promise resolving with the object with includes
* default plug-ins for the specified editor,
* default editor and default components
*/
export async function getDefaultPlugins(): Promise<api.IWorkspacesDefaultPlugins[]> {
const url = `${prefix}/server-config/default-plugins`;
export async function getServerConfig(): Promise<{
defaults: {
plugins: api.IWorkspacesDefaultPlugins[];
components: V220DevfileComponents[];
editor: string | undefined;
};
}> {
const url = `${prefix}/server-config`;
try {
const response = await axios.get(url);
return response.data ? response.data : [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { container } from '../../../../inversify.config';
import { DevWorkspaceBuilder } from '../../../../store/__mocks__/devWorkspaceBuilder';
import { DevWorkspaceClient } from '../devWorkspaceClient';
import * as DwApi from '../../../dashboard-backend-client/devWorkspaceApi';
import * as ServerConfigApi from '../../../dashboard-backend-client/serverConfigApi';
import mockAxios from 'axios';

/* eslint-disable @typescript-eslint/no-non-null-assertion */
Expand Down Expand Up @@ -222,23 +221,4 @@ describe('DevWorkspace client, start', () => {
expect(testWorkspace.spec.template.components![0].plugin!.uri!).toBe(uri);
expect(patchWorkspace).toHaveBeenCalledTimes(0);
});

it('should not call ServerConfigApi.getDefaultPlugins', async () => {
const namespace = 'che';
const name = 'wksp-test';
const testWorkspace = new DevWorkspaceBuilder()
.withMetadata({
name,
namespace,
})
.build();

const getDefaultPlugins = jest.spyOn(ServerConfigApi, 'getDefaultPlugins');
const defaults = {};
const editor = 'eclipse/theia/next';

await client.onStart(testWorkspace, defaults, editor);

expect(getDefaultPlugins).toHaveBeenCalledTimes(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ describe('FactoryResolver store', () => {
it('should create REQUEST_FACTORY_RESOLVER and RECEIVE_FACTORY_RESOLVER', async () => {
const resolver = {
devfile: {
apiVersion: '1.0.0',
} as che.WorkspaceDevfile,
schemaVersion: '2.0.0',
},
} as factoryResolverStore.ResolverState;

getFactoryResolverSpy.mockResolvedValueOnce(resolver);
Expand All @@ -244,7 +244,7 @@ describe('FactoryResolver store', () => {
{
type: 'RECEIVE_FACTORY_RESOLVER',
resolver: expect.objectContaining(resolver),
converted: expect.objectContaining({ isConverted: false }),
converted: expect.objectContaining({ isConverted: true }),
},
];
expect(actions).toEqual(expectedActions);
Expand Down
23 changes: 7 additions & 16 deletions packages/dashboard-frontend/src/store/FactoryResolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ import {
selectDevworkspacesEnabled,
selectPreferredStorageType,
} from '../Workspaces/Settings/selectors';
import { selectDefaultComponents } from '../ServerConfig/selectors';
import { Devfile } from '../../services/workspace-adapter';
import devfileApi, { isDevfileV2 } from '../../services/devfileApi';
import {
convertDevfileV1toDevfileV2,
convertDevfileV2toDevfileV1,
} from '../../services/devfile/converters';
import { convertDevfileV1toDevfileV2 } from '../../services/devfile/converters';
import normalizeDevfileV2 from './normalizeDevfileV2';
import normalizeDevfileV1 from './normalizeDevfileV1';

Expand Down Expand Up @@ -60,7 +58,6 @@ export interface ResolverState extends FactoryResolver {

export interface ConvertedState {
resolvedDevfile: Devfile;
devfileV1: che.WorkspaceDevfile;
devfileV2: devfileApi.Devfile;
isConverted: boolean;
}
Expand Down Expand Up @@ -174,39 +171,33 @@ export const actionCreators: ActionCreators = {
const preferredStorageType = selectPreferredStorageType(state);
const resolvedDevfile = data.devfile;
const isResolvedDevfileV2 = isDevfileV2(resolvedDevfile);
let devfileV1: che.WorkspaceDevfile;
let devfileV2: devfileApi.Devfile;
const defaultComponents = selectDefaultComponents(state);
if (isResolvedDevfileV2) {
devfileV2 = normalizeDevfileV2(resolvedDevfile, data, location);
devfileV1 = normalizeDevfileV1(
await convertDevfileV2toDevfileV1(devfileV2, optionalFilesContent),
preferredStorageType,
);
devfileV2 = normalizeDevfileV2(resolvedDevfile, data, location, defaultComponents);
} else {
devfileV1 = normalizeDevfileV1(resolvedDevfile, preferredStorageType);
const devfileV1 = normalizeDevfileV1(resolvedDevfile, preferredStorageType);
devfileV2 = normalizeDevfileV2(
await convertDevfileV1toDevfileV2(devfileV1),
data,
location,
defaultComponents,
);
}
const converted: ConvertedState = {
resolvedDevfile,
isConverted:
(isDevworkspacesEnabled && isResolvedDevfileV2 === false) ||
(isDevworkspacesEnabled === false && isResolvedDevfileV2),
devfileV1,
devfileV2,
};

const devfile: Devfile = isDevworkspacesEnabled ? devfileV2 : devfileV1;

dispatch({
type: 'RECEIVE_FACTORY_RESOLVER',
resolver: {
...data,
location,
devfile,
devfile: devfileV2,
optionalFilesContent,
},
converted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,27 @@ import {
DEVWORKSPACE_DEVFILE_SOURCE,
DEVWORKSPACE_METADATA_ANNOTATION,
} from '../../services/workspace-client/devworkspace/devWorkspaceClient';
import { V220DevfileComponents } from '@devfile/api';

/**
* Returns a devfile from the FactoryResolver object.
* @param devfile a Devfile.
* @param data a FactoryResolver object.
* @param location a source location.
* @param isDevworkspacesEnabled indicates if devworkspace engine is enabled.
* @param defaultComponents Default components. These default components
* are meant to be used when a Devfile does not contain any components.
*/
export default function normalizeDevfileV2(
devfile: devfileApi.Devfile,
data: FactoryResolver,
location: string,
defaultComponents: V220DevfileComponents[],
): devfileApi.Devfile {
const scmInfo = data['scm_info'];

devfile = devfile as devfileApi.Devfile;
if (!devfile.components) {
devfile.components = [];
if (!devfile.components || devfile.components.length === 0) {
devfile.components = defaultComponents;
}

// temporary solution for fix che-server serialization bug with empty volume
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,27 @@ describe('dwPlugins store', () => {
});

it('should create REQUEST_DW_DEFAULT_EDITOR and RECEIVE_DW_DEFAULT_EDITOR when fetching default plugins', async () => {
(mockAxios.get as jest.Mock).mockResolvedValueOnce({
data: [{ editor: 'eclipse/theia/next', plugins: ['https://test.com/devfile.yaml'] }],
});

const store = new FakeStoreBuilder().build() as MockStoreEnhanced<
const store = new FakeStoreBuilder()
.withDwServerConfig({
defaults: {
editor: 'eclipse/theia/next',
components: [
{
name: 'universal-developer-image',
container: {
image: 'quay.io/devfile/universal-developer-image:ubi8-latest',
},
},
],
plugins: [
{
editor: 'eclipse/theia/next',
plugins: ['https://test.com/devfile.yaml'],
},
],
},
})
.build() as MockStoreEnhanced<
AppState,
ThunkDispatch<AppState, undefined, dwPluginsStore.KnownAction>
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import devfileApi from '../../../services/devfileApi';
import { AppThunk } from '../..';
import { fetchDevfile, fetchData } from '../../../services/registry/devfiles';
import { createObject } from '../../helpers';
import * as ServerConfigApi from '../../../services/dashboard-backend-client/serverConfigApi';

export interface PluginDefinition {
plugin?: devfileApi.Devfile;
Expand Down Expand Up @@ -205,9 +204,11 @@ export const actionCreators: ActionCreators = {

requestDwDefaultEditor:
(settings: che.WorkspaceSettings): AppThunk<KnownAction, Promise<void>> =>
async (dispatch): Promise<void> => {
const defaultEditor = settings['che.factory.default_editor'];

async (dispatch, getState): Promise<void> => {
const config = getState().dwServerConfig.config;
const defaultEditor = config.defaults.editor
? config.defaults.editor
: settings['che.factory.default_editor'];
dispatch({
type: 'REQUEST_DW_DEFAULT_EDITOR',
});
Expand Down Expand Up @@ -238,14 +239,14 @@ export const actionCreators: ActionCreators = {

requestDwDefaultPlugins:
(): AppThunk<KnownAction, Promise<void>> =>
async (dispatch): Promise<void> => {
async (dispatch, getState): Promise<void> => {
dispatch({
type: 'REQUEST_DW_DEFAULT_PLUGINS',
});

const defaultPlugins = {};
const defaults = await ServerConfigApi.getDefaultPlugins();
defaults.forEach(item => {
const defaults = getState().dwServerConfig.config.defaults;
defaults.plugins.forEach(item => {
if (!defaultPlugins[item.editor]) {
defaultPlugins[item.editor] = [];
}
Expand Down
Loading