Merge branch '21128-toolbar-context-menu'
[arvados-workbench2.git] / src / common / config.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import Axios from 'axios';
6
7 export const WORKBENCH_CONFIG_URL =
8     process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
9
10 interface WorkbenchConfig {
11     API_HOST: string;
12     VOCABULARY_URL?: string;
13     FILE_VIEWERS_CONFIG_URL?: string;
14 }
15
16 export interface ClusterConfigJSON {
17     API: {
18         UnfreezeProjectRequiresAdmin: boolean
19         MaxItemsPerResponse: number
20     },
21     ClusterID: string;
22     RemoteClusters: {
23         [key: string]: {
24             ActivateUsers: boolean
25             Host: string
26             Insecure: boolean
27             Proxy: boolean
28             Scheme: string
29         }
30     };
31     Mail?: {
32         SupportEmailAddress: string;
33     };
34     Services: {
35         Controller: {
36             ExternalURL: string;
37         };
38         Workbench1: {
39             ExternalURL: string;
40         };
41         Workbench2: {
42             ExternalURL: string;
43         };
44         Workbench: {
45             DisableSharingURLsUI: boolean;
46             ArvadosDocsite: string;
47             FileViewersConfigURL: string;
48             WelcomePageHTML: string;
49             InactivePageHTML: string;
50             SSHHelpPageHTML: string;
51             SSHHelpHostSuffix: string;
52             SiteName: string;
53             IdleTimeout: string;
54         };
55         Websocket: {
56             ExternalURL: string;
57         };
58         WebDAV: {
59             ExternalURL: string;
60         };
61         WebDAVDownload: {
62             ExternalURL: string;
63         };
64         WebShell: {
65             ExternalURL: string;
66         };
67     };
68     Workbench: {
69         DisableSharingURLsUI: boolean;
70         ArvadosDocsite: string;
71         FileViewersConfigURL: string;
72         WelcomePageHTML: string;
73         InactivePageHTML: string;
74         SSHHelpPageHTML: string;
75         SSHHelpHostSuffix: string;
76         SiteName: string;
77         IdleTimeout: string;
78         BannerUUID: string;
79         UserProfileFormFields: {};
80         UserProfileFormMessage: string;
81     };
82     Login: {
83         LoginCluster: string;
84         Google: {
85             Enable: boolean;
86         };
87         LDAP: {
88             Enable: boolean;
89         };
90         OpenIDConnect: {
91             Enable: boolean;
92         };
93         PAM: {
94             Enable: boolean;
95         };
96         SSO: {
97             Enable: boolean;
98         };
99         Test: {
100             Enable: boolean;
101         };
102     };
103     Collections: {
104         ForwardSlashNameSubstitution: string;
105         ManagedProperties?: {
106             [key: string]: {
107                 Function: string;
108                 Value: string;
109                 Protected?: boolean;
110             };
111         };
112         TrustAllContent: boolean;
113     };
114     Volumes: {
115         [key: string]: {
116             StorageClasses: {
117                 [key: string]: boolean;
118             };
119         };
120     };
121     Users: {
122         AnonymousUserToken: string;
123     };
124 }
125
126 export class Config {
127     baseUrl!: string;
128     keepWebServiceUrl!: string;
129     keepWebInlineServiceUrl!: string;
130     remoteHosts!: {
131         [key: string]: string;
132     };
133     rootUrl!: string;
134     uuidPrefix!: string;
135     websocketUrl!: string;
136     workbenchUrl!: string;
137     workbench2Url!: string;
138     vocabularyUrl!: string;
139     fileViewersConfigUrl!: string;
140     loginCluster!: string;
141     clusterConfig!: ClusterConfigJSON;
142     apiRevision!: number;
143 }
144
145 export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
146     const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
147     const config = new Config();
148     config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
149     config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
150     config.uuidPrefix = clusterConfigJSON.ClusterID;
151     config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
152     config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
153     config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
154     config.keepWebServiceUrl =
155         clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
156     config.keepWebInlineServiceUrl =
157         clusterConfigJSON.Services.WebDAV.ExternalURL;
158     config.loginCluster = clusterConfigJSON.Login.LoginCluster;
159     config.clusterConfig = clusterConfigJSON;
160     config.apiRevision = 0;
161     mapRemoteHosts(clusterConfigJSON, config);
162     return config;
163 };
164
165 export const getStorageClasses = (config: Config): string[] => {
166     const classes: Set<string> = new Set(['default']);
167     const volumes = config.clusterConfig.Volumes;
168     Object.keys(volumes).forEach((v) => {
169         Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
170             if (volumes[v].StorageClasses[sc]) {
171                 classes.add(sc);
172             }
173         });
174     });
175     return Array.from(classes);
176 };
177
178 const getApiRevision = async (apiUrl: string) => {
179     try {
180         const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
181         return parseInt(dd.revision, 10) || 0;
182     } catch {
183         console.warn(
184             'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
185         );
186         return 0;
187     }
188 };
189
190 const removeTrailingSlashes = (
191     config: ClusterConfigJSON
192 ): ClusterConfigJSON => {
193     const svcs: any = {};
194     Object.keys(config.Services).forEach((s) => {
195         svcs[s] = config.Services[s];
196         if (svcs[s].hasOwnProperty('ExternalURL')) {
197             svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
198         }
199     });
200     return { ...config, Services: svcs };
201 };
202
203 export const fetchConfig = () => {
204     return Axios.get<WorkbenchConfig>(
205         WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
206     )
207         .then((response) => response.data)
208         .catch(() => {
209             console.warn(
210                 `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
211             );
212             return Promise.resolve(getDefaultConfig());
213         })
214         .then((workbenchConfig) => {
215             if (workbenchConfig.API_HOST === undefined) {
216                 throw new Error(
217                     `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
218                 );
219             }
220             return Axios.get<ClusterConfigJSON>(
221                 getClusterConfigURL(workbenchConfig.API_HOST)
222             ).then(async (response) => {
223                 const apiRevision = await getApiRevision(
224                     response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
225                 );
226                 const config = { ...buildConfig(response.data), apiRevision };
227                 const warnLocalConfig = (varName: string) =>
228                     console.warn(
229                         `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
230 remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`
231                     );
232
233                 // Check if the workbench config has an entry for vocabulary and file viewer URLs
234                 // If so, use these values (even if it is an empty string), but print a console warning.
235                 // Otherwise, use the cluster config.
236                 let fileViewerConfigUrl;
237                 if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
238                     warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
239                     fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
240                 } else {
241                     fileViewerConfigUrl =
242                         config.clusterConfig.Workbench.FileViewersConfigURL ||
243                         '/file-viewers-example.json';
244                 }
245                 config.fileViewersConfigUrl = fileViewerConfigUrl;
246
247                 if (workbenchConfig.VOCABULARY_URL !== undefined) {
248                     console.warn(
249                         `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
250                     );
251                 }
252                 config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
253
254                 return { config, apiHost: workbenchConfig.API_HOST };
255             });
256         });
257 };
258
259 // Maps remote cluster hosts and removes the default RemoteCluster entry
260 export const mapRemoteHosts = (
261     clusterConfigJSON: ClusterConfigJSON,
262     config: Config
263 ) => {
264     config.remoteHosts = {};
265     Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
266         config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
267     });
268     delete config.remoteHosts['*'];
269 };
270
271 export const mockClusterConfigJSON = (
272     config: Partial<ClusterConfigJSON>
273 ): ClusterConfigJSON => ({
274     API: {
275         UnfreezeProjectRequiresAdmin: false,
276         MaxItemsPerResponse: 1000,
277     },
278     ClusterID: '',
279     RemoteClusters: {},
280     Services: {
281         Controller: { ExternalURL: '' },
282         Workbench1: { ExternalURL: '' },
283         Workbench2: { ExternalURL: '' },
284         Websocket: { ExternalURL: '' },
285         WebDAV: { ExternalURL: '' },
286         WebDAVDownload: { ExternalURL: '' },
287         WebShell: { ExternalURL: '' },
288         Workbench: {
289             DisableSharingURLsUI: false,
290             ArvadosDocsite: "",
291             FileViewersConfigURL: "",
292             WelcomePageHTML: "",
293             InactivePageHTML: "",
294             SSHHelpPageHTML: "",
295             SSHHelpHostSuffix: "",
296             SiteName: "",
297             IdleTimeout: "0s"
298         },
299     },
300     Workbench: {
301         DisableSharingURLsUI: false,
302         ArvadosDocsite: '',
303         FileViewersConfigURL: '',
304         WelcomePageHTML: '',
305         InactivePageHTML: '',
306         SSHHelpPageHTML: '',
307         SSHHelpHostSuffix: '',
308         SiteName: '',
309         IdleTimeout: '0s',
310         BannerUUID: "",
311         UserProfileFormFields: {},
312         UserProfileFormMessage: '',
313     },
314     Login: {
315         LoginCluster: '',
316         Google: {
317             Enable: false,
318         },
319         LDAP: {
320             Enable: false,
321         },
322         OpenIDConnect: {
323             Enable: false,
324         },
325         PAM: {
326             Enable: false,
327         },
328         SSO: {
329             Enable: false,
330         },
331         Test: {
332             Enable: false,
333         },
334     },
335     Collections: {
336         ForwardSlashNameSubstitution: '',
337         TrustAllContent: false,
338     },
339     Volumes: {},
340     Users: {
341         AnonymousUserToken: ""
342     },
343     ...config,
344 });
345
346 export const mockConfig = (config: Partial<Config>): Config => ({
347     baseUrl: '',
348     keepWebServiceUrl: '',
349     keepWebInlineServiceUrl: '',
350     remoteHosts: {},
351     rootUrl: '',
352     uuidPrefix: '',
353     websocketUrl: '',
354     workbenchUrl: '',
355     workbench2Url: '',
356     vocabularyUrl: '',
357     fileViewersConfigUrl: '',
358     loginCluster: '',
359     clusterConfig: mockClusterConfigJSON({}),
360     apiRevision: 0,
361     ...config,
362 });
363
364 const getDefaultConfig = (): WorkbenchConfig => {
365     let apiHost = '';
366     const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
367     if (envHost !== undefined) {
368         console.warn(`Using default API host ${envHost}.`);
369         apiHost = envHost;
370     } else {
371         console.warn(
372             `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
373         );
374     }
375     return {
376         API_HOST: apiHost,
377         VOCABULARY_URL: undefined,
378         FILE_VIEWERS_CONFIG_URL: undefined,
379     };
380 };
381
382 export const ARVADOS_API_PATH = 'arvados/v1';
383 export const CLUSTER_CONFIG_PATH = 'arvados/v1/config';
384 export const VOCABULARY_PATH = 'arvados/v1/vocabulary';
385 export const DISCOVERY_DOC_PATH = 'discovery/v1/apis/arvados/v1/rest';
386 export const getClusterConfigURL = (apiHost: string) =>
387     `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
388 export const getVocabularyURL = (apiHost: string) =>
389     `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;