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