Expanded open file functionality to use available apps.
[pithos-ios] / Classes / LBNodesViewController.m
1 //
2 //  LBNodesViewController.m
3 //  OpenStack
4 //
5 //  Created by Mike Mayo on 5/4/11.
6 //  Copyright 2011 __MyCompanyName__. All rights reserved.
7 //
8
9 #import "LBNodesViewController.h"
10 #import "OpenStackAccount.h"
11 #import "AccountManager.h"
12 #import "LoadBalancer.h"
13 #import "LoadBalancerNode.h"
14 #import "Server.h"
15 #import "Flavor.h"
16 #import "Image.h"
17 #import "RSTextFieldCell.h"
18 #import "UIViewController+Conveniences.h"
19 #import "LBServersViewController.h"
20 #import "LoadBalancerProtocol.h"
21 #import "ActivityIndicatorView.h"
22 #import "APICallback.h"
23 #import "AnimatedProgressView.h"
24
25 #define kNodes 0
26 #define kCloudServers 1
27
28 @implementation LBNodesViewController
29
30 @synthesize account, loadBalancer, isNewLoadBalancer;
31
32 - (void)dealloc {
33     [account release];
34     [loadBalancer release];
35     [ipNodes release];
36     [cloudServerNodes release];
37     [nodesToDelete release];
38     [super dealloc];
39 }
40
41 #pragma mark - Utilities
42
43 - (void)deleteEmptyIPRows {
44     NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
45     NSMutableArray *nodesToRemove = [[NSMutableArray alloc] init];
46     
47     for (int i = 0; i < [ipNodes count]; i++) {
48         LoadBalancerNode *node = [self.loadBalancer.nodes objectAtIndex:i];
49         if (!node.address || [node.address isEqualToString:@""]) {
50             NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:kNodes];
51             [indexPaths addObject:indexPath];
52             [nodesToRemove addObject:node];
53         }
54     }
55     
56     for (LoadBalancerNode *node in nodesToRemove) {
57         [self.loadBalancer.nodes removeObject:node];
58     }
59     
60     [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
61     [indexPaths release];
62     [nodesToRemove release];
63 }
64
65 #pragma mark - View lifecycle
66
67 - (void)viewDidLoad {
68     [super viewDidLoad];
69     saved = NO;
70     self.navigationItem.title = @"Nodes";
71     textFields = [[NSMutableArray alloc] init];
72     ipNodes = [[NSMutableArray alloc] init];
73     cloudServerNodes = [[NSMutableArray alloc] init];
74     NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:[self.loadBalancer.nodes count]];
75     for (LoadBalancerNode *node in self.loadBalancer.nodes) {
76         LoadBalancerNode *copiedNode = node; //[node copy];
77         [nodes addObject:copiedNode];
78         if (copiedNode.server) {
79             [cloudServerNodes addObject:node];
80         } else {
81             [ipNodes addObject:node];
82         }
83         //[copiedNode release];
84     }
85     previousNodes = [[NSArray alloc] initWithArray:nodes];
86     [nodes release];        
87 }
88
89 - (void)viewWillAppear:(BOOL)animated {
90     [super viewWillAppear:animated];
91     [self.tableView reloadData];
92     if (!isNewLoadBalancer) {
93         [self addSaveButton];
94     }
95 }
96
97 - (void)viewWillDisappear:(BOOL)animated {
98     [super viewWillDisappear:animated];
99     if (isNewLoadBalancer) {
100         NSMutableArray *finalNodes = [[NSMutableArray alloc] init];
101         for (LoadBalancerNode *node in ipNodes) {
102             if (node.address && ![node.address isEqualToString:@""]) {
103                 [finalNodes addObject:node];
104             }
105         }
106         for (LoadBalancerNode *node in cloudServerNodes) {
107             [finalNodes addObject:node];
108         }
109         if ([finalNodes count] > 0) {
110             self.loadBalancer.nodes = [[[NSMutableArray alloc] initWithArray:finalNodes] autorelease];
111         }
112         [finalNodes release];
113         self.navigationItem.rightBarButtonItem = nil;    
114     } else {
115         if (!saved) {
116             self.loadBalancer.nodes = [NSMutableArray arrayWithArray:previousNodes];
117         }
118     }
119 }
120
121 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
122     return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || (toInterfaceOrientation == UIInterfaceOrientationPortrait);
123 }
124
125 #pragma mark - Table view data source
126
127 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
128     return 2;
129 }
130
131 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
132     if (section == kNodes) {
133         return [ipNodes count] + 1;
134     } else {
135         return [cloudServerNodes count] + 1;
136     }
137 }
138
139 - (RSTextFieldCell *)tableView:(UITableView *)tableView ipCellForRowAtIndexPath:(NSIndexPath *)indexPath {
140     NSString *CellIdentifier = [NSString stringWithFormat:@"IPCell%i", indexPath.row];
141     
142     RSTextFieldCell *cell = (RSTextFieldCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
143     if (cell == nil) {
144         cell = [[[RSTextFieldCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
145         cell.textField.delegate = self;
146         
147         // tag it so we'll know which node we're editing
148         cell.textField.tag = indexPath.row;
149         
150         cell.textField.returnKeyType = UIReturnKeyDone;
151         cell.textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
152         [textFields addObject:cell.textField];
153         
154         cell.imageView.image = [UIImage imageNamed:@"red-delete-button.png"];        
155     }
156
157     if (indexPath.row < [ipNodes count]) {
158         LoadBalancerNode *node = [ipNodes objectAtIndex:indexPath.row];
159         cell.textField.text = node.address;
160     }
161     
162     return cell;
163 }
164
165 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
166     static NSString *CellIdentifier = @"Cell";
167     
168     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
169     if (cell == nil) {
170         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
171     }
172     
173     // Configure the cell...
174     cell.accessoryType = UITableViewCellAccessoryNone;
175     if (indexPath.section == kNodes) {
176         if (indexPath.row == [ipNodes count]) {
177             cell.textLabel.text = @"Add IP Address";
178             cell.detailTextLabel.text = @"";
179             cell.imageView.image = [UIImage imageNamed:@"green-add-button.png"];
180         } else {
181             return [self tableView:tableView ipCellForRowAtIndexPath:indexPath];
182         }
183     } else if (indexPath.section == kCloudServers) {
184         if (indexPath.row == [cloudServerNodes count]) {
185             if ([cloudServerNodes count] == 0) {
186                 cell.textLabel.text = @"Add Cloud Servers";
187             } else {
188                 cell.textLabel.text = @"Add/Remove Cloud Servers";
189             }
190             cell.detailTextLabel.text = @"";
191             cell.imageView.image = [UIImage imageNamed:@"green-add-button.png"];
192             if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
193                 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
194             } else {
195                 cell.accessoryType = UITableViewCellAccessoryNone;
196             }
197         } else {
198             Server *server = [[cloudServerNodes objectAtIndex:indexPath.row] server];
199             cell.textLabel.text = server.name;
200             cell.detailTextLabel.text = server.flavor.name;
201             if ([server.image respondsToSelector:@selector(logoPrefix)] && [[server.image logoPrefix] isEqualToString:kCustomImage]) {
202                 cell.imageView.image = [UIImage imageNamed:kCloudServersIcon];
203             } else {
204                 if ([server.image respondsToSelector:@selector(logoPrefix)]) {
205                     cell.imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-icon.png", [server.image logoPrefix]]];
206                 }
207             }
208         }
209     }
210     
211     return cell;
212 }
213
214 // Override to support conditional editing of the table view.
215 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
216     return indexPath.section == kNodes && indexPath.row == [ipNodes count];
217 }
218
219 #pragma mark - Table view delegate
220
221 - (void)focusOnLastTextField {
222     NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[textFields count] - 1 inSection:kNodes];
223     [[textFields lastObject] becomeFirstResponder];
224     [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
225 }
226
227 - (void)addIPRow {
228     [self deleteEmptyIPRows];
229     LoadBalancerNode *node = [[[LoadBalancerNode alloc] init] autorelease];
230     node.condition = @"ENABLED";
231     node.port = [NSString stringWithFormat:@"%i", self.loadBalancer.protocol.port];
232     [ipNodes addObject:node];
233     [self.loadBalancer.nodes addObject:node];
234     NSArray *indexPath = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:[ipNodes count] - 1 inSection:kNodes]];
235     [self.tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationBottom];
236     [NSTimer scheduledTimerWithTimeInterval:0.35 target:self selector:@selector(focusOnLastTextField) userInfo:nil repeats:NO];
237 }
238
239 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
240     if (indexPath.section == kNodes) {
241         if (indexPath.row == [ipNodes count]) {
242             [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
243             [self addIPRow];
244         } else {
245             [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
246             NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
247             LoadBalancerNode *node = [ipNodes objectAtIndex:indexPath.row];
248             [ipNodes removeObject:node];
249             [self.loadBalancer.nodes removeObject:node];
250             [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
251         }
252     } else if (indexPath.section == kCloudServers) {
253         LBServersViewController *vc = [[LBServersViewController alloc] initWithAccount:self.account loadBalancer:self.loadBalancer serverNodes:cloudServerNodes];
254         if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
255             [self.navigationController pushViewController:vc animated:YES];
256         } else {
257             [self presentModalViewControllerWithNavigation:vc];
258         }
259         [vc release];
260     }
261 }
262
263 #pragma mark - Text field delegate
264
265 - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
266 //    if (isNewLoadBalancer) {
267 //        [self addDoneButton];
268 //    }
269     return YES;
270 }
271
272 - (BOOL)textFieldShouldReturn:(UITextField *)textField {
273     [textField resignFirstResponder]; 
274     
275     if ([textField.text isEqualToString:@""]) {
276         [self deleteEmptyIPRows];        
277     }
278     
279     return NO;
280 }
281
282 - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {    
283     LoadBalancerNode *node = [ipNodes objectAtIndex:textField.tag];    
284     node.address = [textField.text stringByReplacingCharactersInRange:range withString:string];
285     return YES;
286 }
287
288 #pragma mark - Button Handlers
289
290 //- (void)doneButtonPressed:(id)sender {
291 //    for (UITextField *textField in textFields) {
292 //        [textField resignFirstResponder];
293 //    }
294 //    self.navigationItem.rightBarButtonItem = nil;
295 //}
296
297 - (void)deleteNodeProgress {
298     currentAPICalls++;
299     NSLog(@"((%i / 1.0) / %i) = %f", currentAPICalls, totalAPICalls, ((currentAPICalls / 1.0) / totalAPICalls));
300     [spinner.progressView setProgress:((currentAPICalls / 1.0) / totalAPICalls) animated:YES];
301     
302     if (currentAPICalls == totalAPICalls) {
303         [spinner removeFromSuperviewAndRelease]; 
304     }
305 }
306
307 - (void)deleteNode:(LoadBalancerNode *)node {
308     
309     NSLog(@"trying to delete node %@", node.identifier);
310     
311     NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];
312     
313     APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint];
314     
315     __block void (^callBackBlock)(OpenStackRequest *request);        
316     callBackBlock = ^(OpenStackRequest *request) {
317         
318         deleteIndex++;
319         [self deleteNodeProgress];
320
321         if (![request isSuccess]) {
322             [self alert:@"There was a problem deleting a node." request:request];
323         } else {
324             self.loadBalancer.status = @"PENDING_UPDATE";
325         }
326         
327         
328         if (deleteIndex < [nodesToDelete count]) {
329
330             self.loadBalancer.status = @"PENDING_UPDATE";            
331             [self.loadBalancer pollUntilActive:self.account delegate:self completeSelector:@selector(deleteNode:) object:[nodesToDelete objectAtIndex:deleteIndex]];
332             
333             //                [self.loadBalancer pollUntilActive:self.account complete:^{
334             //                    deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]);
335             //                }];
336         };
337         
338     };
339     
340     [callback success:callBackBlock failure:callBackBlock];    
341 }
342
343 - (void)deleteNodesWithProgress:(ASIBasicBlock)progressBlock {
344     
345     NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];
346     deleteIndex = 0;
347     
348     __block void (^deleteNodeBlock)(LoadBalancerNode *node);
349     deleteNodeBlock = ^(LoadBalancerNode *node) {
350         
351         NSLog(@"trying to delete node %@", node.identifier);
352         
353         APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint];
354         
355         __block void (^callBackBlock)(OpenStackRequest *request);        
356         callBackBlock = ^(OpenStackRequest *request) {
357             
358             deleteIndex++;
359             [self deleteNodeProgress];
360             
361             if (deleteIndex < [nodesToDelete count]) {
362                 self.loadBalancer.status = @"PENDING_UPDATE";            
363                 [self.loadBalancer pollUntilActive:self.account delegate:self completeSelector:@selector(deleteNode:) object:[nodesToDelete objectAtIndex:deleteIndex]];
364                 
365 //                [self.loadBalancer pollUntilActive:self.account complete:^{
366 //                    deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]);
367 //                }];
368             };
369             
370             if (![request isSuccess]) {
371                 [self alert:@"There was a problem deleting a node." request:request];
372             }
373         };
374
375         [callback success:callBackBlock failure:callBackBlock];
376         
377     };
378
379     LoadBalancerNode *node = [nodesToDelete objectAtIndex:deleteIndex];
380     deleteNodeBlock(node);
381     
382 }
383     
384 - (void)addNodes:(NSArray *)nodesToAdd andDeleteNodesWithProgress:(ASIBasicBlock)progressBlock failure:(APIResponseBlock)failureBlock {
385
386     NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];    
387     
388     if ([nodesToAdd count] > 0) {
389         // we want to add before doing any deletes to avoid attempting an invalid delete
390         APICallback *callback = [self.account.manager addLBNodes:nodesToAdd loadBalancer:self.loadBalancer endpoint:endpoint];
391         [callback success:^(OpenStackRequest *request) {
392             
393             // if it's a successful add, the status will be PENDING_UPDATE.  cheaper
394             // to just set it than hit the API again since we're already going to hit it
395             // n times for the deletes
396             self.loadBalancer.status = @"PENDING_UPDATE";
397
398             [self deleteNodeProgress];
399             
400             if ([nodesToDelete count] > 0) {            
401                 // before you delete, you need to poll the LB until it hits active status
402                 [self.loadBalancer pollUntilActive:self.account complete:^{
403                     [self deleteNodesWithProgress:progressBlock];
404                 }];
405             }
406             
407         } failure:^(OpenStackRequest *request) {
408             failureBlock(request);
409         }];
410     } else {
411         [self deleteNodesWithProgress:progressBlock];
412     }
413     
414 }
415
416 - (void)saveButtonPressed:(id)sender {
417     
418     if ([self.loadBalancer.nodes count] == 0) {
419         [self alert:nil message:@"You must have at least one node attached to this load balancer."];
420         return;
421     } else {
422         NSInteger enabledCount = 0;
423         for (LoadBalancerNode *node in self.loadBalancer.nodes) {
424             if ([node.condition isEqualToString:@"ENABLED"]) {
425                 enabledCount++;
426             }
427         }
428         if (enabledCount == 0) {
429             [self alert:nil message:@"You must have at least one enabled node attached to this load balancer."];
430             return;
431         }
432     }
433     
434     
435     
436     // we need to compare the previousNodoes list to the current nodes list so we
437     // can know which nodes to add and which ones to delete
438     NSMutableArray *nodesToAdd = [[NSMutableArray alloc] init];
439     nodesToDelete = [[NSMutableArray alloc] init];
440     
441     NSLog(@"previous nodes: %@", previousNodes);
442     NSLog(@"lb nodes: %@", self.loadBalancer.nodes);
443     
444     for (LoadBalancerNode *node in previousNodes) {
445         if (![self.loadBalancer.nodes containsObject:node]) {
446             [nodesToDelete addObject:node];
447             NSLog(@"going to delete node: %@", node);
448         }
449     }
450     
451     for (LoadBalancerNode *node in self.loadBalancer.nodes) {
452         if (![previousNodes containsObject:node]) {
453             [nodesToAdd addObject:node];
454             NSLog(@"going to add node: %@", node);
455         }
456     }
457
458     currentAPICalls = 0;
459     totalAPICalls = [nodesToDelete count] + ([nodesToAdd count] > 0 ? 1 : 0);
460
461     if (totalAPICalls > 0) {
462         spinner = [[ActivityIndicatorView alloc] initWithFrame:[ActivityIndicatorView frameForText:@"Saving..." withProgress:YES] text:@"Saving..." withProgress:YES];
463         [spinner addToView:self.view];
464         
465         saved = YES;
466         
467         // make the API calls
468         [self addNodes:nodesToAdd andDeleteNodesWithProgress:^{
469             currentAPICalls++;
470             // TODO: update progress view on spinner
471             if (currentAPICalls == totalAPICalls) {
472                 [spinner removeFromSuperviewAndRelease]; 
473             }
474         } failure:^(OpenStackRequest *request) {
475             [self alert:@"There was a problem adding nodes." request:request];
476             [spinner removeFromSuperviewAndRelease];
477         }];
478     } else {
479         [self alert:nil message:@"You did not select any nodes to add or remove."];
480     }
481     
482     [nodesToAdd release];
483 }
484
485 @end