18284: Resolve bugs in VM listing and add login administration functions
[arvados-workbench2.git] / src / store / virtual-machines / virtual-machines-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from "redux";
6 import { RootState } from 'store/store';
7 import { ServiceRepository } from "services/services";
8 import { navigateToUserVirtualMachines, navigateToAdminVirtualMachines, navigateToRootProject } from "store/navigation/navigation-action";
9 import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-action';
10 import { formatDate } from "common/formatters";
11 import { unionize, ofType, UnionOf } from "common/unionize";
12 import { VirtualMachineLogins } from 'models/virtual-machines';
13 import { FilterBuilder } from "services/api/filter-builder";
14 import { ListResults } from "services/common-service/common-service";
15 import { dialogActions } from 'store/dialog/dialog-actions';
16 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
17 import { PermissionLevel } from "models/permission";
18 import { deleteResources, updateResources } from 'store/resources/resources-actions';
19 import { Participant } from "views-components/sharing-dialog/participant-select";
20 import { initialize, reset } from "redux-form";
21
22 export const virtualMachinesActions = unionize({
23     SET_REQUESTED_DATE: ofType<string>(),
24     SET_VIRTUAL_MACHINES: ofType<ListResults<any>>(),
25     SET_LOGINS: ofType<VirtualMachineLogins>(),
26     SET_LINKS: ofType<ListResults<any>>()
27 });
28
29 export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
30
31 export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
32 export const VIRTUAL_MACHINE_ATTRIBUTES_DIALOG = 'virtualMachineAttributesDialog';
33 export const VIRTUAL_MACHINE_REMOVE_DIALOG = 'virtualMachineRemoveDialog';
34 export const VIRTUAL_MACHINE_ADD_LOGIN_DIALOG = 'virtualMachineAddLoginDialog';
35 export const VIRTUAL_MACHINE_ADD_LOGIN_FORM = 'virtualMachineAddLoginForm';
36 export const VIRTUAL_MACHINE_REMOVE_LOGIN_DIALOG = 'virtualMachineRemoveLoginDialog';
37
38 export const VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD = 'vmUuid';
39 export const VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD = 'user';
40 export const VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD = 'groups';
41
42 export const openUserVirtualMachines = () =>
43     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
44         dispatch<any>(navigateToUserVirtualMachines);
45     };
46
47 export const openAdminVirtualMachines = () =>
48     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
49         const user = getState().auth.user;
50         if (user && user.isAdmin) {
51             dispatch<any>(navigateToAdminVirtualMachines);
52         } else {
53             dispatch<any>(navigateToRootProject);
54             dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000, kind: SnackbarKind.ERROR }));
55         }
56     };
57
58 export const openVirtualMachineAttributes = (uuid: string) =>
59     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
60         const virtualMachineData = getState().virtualMachines.virtualMachines.items.find(it => it.uuid === uuid);
61         dispatch(dialogActions.OPEN_DIALOG({ id: VIRTUAL_MACHINE_ATTRIBUTES_DIALOG, data: { virtualMachineData } }));
62     };
63
64 const loadRequestedDate = () =>
65     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
66         const date = services.virtualMachineService.getRequestedDate();
67         dispatch(virtualMachinesActions.SET_REQUESTED_DATE(date));
68     };
69
70 export const loadVirtualMachinesAdminData = () =>
71     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
72         dispatch<any>(loadRequestedDate());
73
74         const virtualMachines = await services.virtualMachineService.list();
75         dispatch(updateResources(virtualMachines.items));
76         dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
77
78
79         const logins = await services.permissionService.list({
80             filters: new FilterBuilder()
81             .addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
82             .addEqual('name', PermissionLevel.CAN_LOGIN)
83             .getFilters()
84         });
85         dispatch(updateResources(logins.items));
86         dispatch(virtualMachinesActions.SET_LINKS(logins));
87
88         const users = await services.userService.list({
89             filters: new FilterBuilder()
90             .addIn('uuid', logins.items.map(item => item.tailUuid))
91             .getFilters(),
92             count: "none"
93         });
94         dispatch(updateResources(users.items));
95
96         const getAllLogins = await services.virtualMachineService.getAllLogins();
97         dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins));
98     };
99
100 export const loadVirtualMachinesUserData = () =>
101     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
102         dispatch<any>(loadRequestedDate());
103         const virtualMachines = await services.virtualMachineService.list();
104         const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
105         const links = await services.linkService.list({
106             filters: new FilterBuilder()
107                 .addIn("head_uuid", virtualMachinesUuids)
108                 .getFilters()
109         });
110         dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
111         dispatch(virtualMachinesActions.SET_LINKS(links));
112     };
113
114 export const openAddVirtualMachineLoginDialog = (uuid: string) =>
115     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
116         dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, {[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: uuid, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: ['docker']}));
117         dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {}} ));
118     }
119
120 export interface AddLoginFormData {
121     [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: string;
122     [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: Participant;
123     [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: string[];
124 }
125
126
127 export const addVirtualMachineLogin = ({vmUuid, user, groups}: AddLoginFormData) =>
128     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
129         try {
130             // Get user
131             const userResource = await services.userService.get(user.uuid);
132
133             const permission = await services.permissionService.create({
134                 headUuid: vmUuid,
135                 tailUuid: userResource.uuid,
136                 name: PermissionLevel.CAN_LOGIN,
137                 properties: {
138                     username: userResource.username,
139                     groups,
140                 }
141             });
142             dispatch(updateResources([permission]));
143
144             dispatch(reset(VIRTUAL_MACHINE_ADD_LOGIN_FORM));
145             dispatch(dialogActions.CLOSE_DIALOG({ id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG }));
146             dispatch<any>(loadVirtualMachinesAdminData());
147
148             dispatch(snackbarActions.OPEN_SNACKBAR({
149                 message: `Permissions updated`,
150                 kind: SnackbarKind.SUCCESS
151             }));
152         } catch (e) {
153             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
154         }
155     };
156
157 export const openRemoveVirtualMachineLoginDialog = (uuid: string) =>
158     (dispatch: Dispatch, getState: () => RootState) => {
159         dispatch(dialogActions.OPEN_DIALOG({
160             id: VIRTUAL_MACHINE_REMOVE_LOGIN_DIALOG,
161             data: {
162                 title: 'Remove login permission',
163                 text: 'Are you sure you want to remove this permission?',
164                 confirmButtonLabel: 'Remove',
165                 uuid
166             }
167         }));
168     };
169
170 export const removeVirtualMachineLogin = (uuid: string) =>
171     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
172         try {
173             await services.permissionService.delete(uuid);
174             dispatch<any>(deleteResources([uuid]));
175
176             dispatch<any>(loadVirtualMachinesAdminData());
177
178             dispatch(snackbarActions.OPEN_SNACKBAR({
179                 message: `Login permission removed`,
180                 kind: SnackbarKind.SUCCESS
181             }));
182         } catch (e) {
183             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
184         }
185     };
186
187 export const saveRequestedDate = () =>
188     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
189         const date = formatDate((new Date()).toISOString());
190         services.virtualMachineService.saveRequestedDate(date);
191         dispatch<any>(loadRequestedDate());
192     };
193
194 export const openRemoveVirtualMachineDialog = (uuid: string) =>
195     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
196         dispatch(dialogActions.OPEN_DIALOG({
197             id: VIRTUAL_MACHINE_REMOVE_DIALOG,
198             data: {
199                 title: 'Remove virtual machine',
200                 text: 'Are you sure you want to remove this virtual machine?',
201                 confirmButtonLabel: 'Remove',
202                 uuid
203             }
204         }));
205     };
206
207 export const removeVirtualMachine = (uuid: string) =>
208     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
209         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
210         await services.virtualMachineService.delete(uuid);
211         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
212         dispatch<any>(loadVirtualMachinesAdminData());
213     };
214
215 const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
216
217 export const loadVirtualMachinesPanel = () =>
218     (dispatch: Dispatch) => {
219         dispatch(virtualMachinesBindedActions.REQUEST_ITEMS());
220     };