Compare commits

..

200 Commits

Author SHA1 Message Date
johnytiago
078513f603 feat(instances): adds cli-details UI 2018-06-04 14:30:07 +01:00
Fábio Moreira
a1ff036db1 test(instances): updates snapshots 2018-06-04 12:48:09 +01:00
Fábio Moreira
fc81d666d0 feat(instances): CI - firewall responsive designs 2018-06-04 12:48:09 +01:00
Fábio Moreira
deb1a8436f feat(instances): user scripts responsive designs 2018-06-04 12:48:09 +01:00
Fábio Moreira
cad1431e79 feat(instances): CI - metadata resposive 2018-06-04 12:48:09 +01:00
Fábio Moreira
2f9d135319 feat(instances): CI - Responsive behavior for tags #1121 2018-06-04 12:48:09 +01:00
Fábio Moreira
c268d88a4d feat(instances): create instance - networks responsive next button 2018-06-04 12:48:09 +01:00
Fábio Moreira
6698a8eacb feat(ui-toolkit): key-value responsive 2018-06-04 12:48:09 +01:00
Fábio Moreira
1e8e89b3c8 feat(instances): theme resizer 2018-06-04 12:48:09 +01:00
Sérgio Ramos
063e40859d feat(sg): bootstrap 2018-06-04 11:46:08 +01:00
Sérgio Ramos
fc84358dff feat(templates): bootstrap 2018-06-04 11:46:08 +01:00
Fábio Moreira
6736caaf45
Implement responsive designs for CI: Networks (#1438)
* feat(instances): network section responive behaviour

* feat(instances): adjust network info container width

* test(instances): update network step snapshot

* test(instances): update network step snapshots
2018-05-28 13:38:21 +01:00
Fábio Moreira
5cb02d709c feat(instances): responsive layout for instance name card
fixes #1118
2018-05-24 10:34:55 +01:00
Fábio Moreira
b66f761a9e
#1406 - implement responsive rules on create instance (#1432)
* fix(instances): swap absolute media query value for defined breakpoint

* feat(ui-toolkit): update breakpoints

* feat(ui-toolkit): view container padding and max width

* feat(ui-toolkit): responsive image selector

* fix(ui-toolkit): delay parallax header hiding

* feat(instances): responsive rules for package selection

* test(instances): update snapshots

* fix(instances): use color variables on package card

* fix(instances): import breakpoints from ui-toolkit

* feat(instances): package selection for mobile or desktop version

* feat(instances): remove unnecessary col css props

* test(instances): update snapshots

* test(instances): update resource widgets snaphots
2018-05-23 12:41:40 +01:00
johnytiago
8422cdfe8c feat(instances): QA Instance Managment bug fixes. closes #1423 2018-05-21 11:37:26 +01:00
Joao Tiago
109988536b feat(instances): Fixes collase typo 2018-05-18 13:48:09 +01:00
johnytiago
d75ae0f14f feat(instances): Adds font antialiasing. closes #1418 2018-05-18 13:47:27 +01:00
johnytiago
5c98a4cecb feat(instances): refactor card headers #1417 2018-05-17 20:01:44 +01:00
johnytiago
0189822a08 feat(instances): bug fixes #1387 2018-05-17 18:14:00 +01:00
johnytiago
5d46689869 feat(instances): Adds better element ids 2018-05-17 18:03:08 +01:00
johnytiago
04cb9c32f8 feat(instances): Firewall empty state update, more QA 2018-05-17 17:50:11 +01:00
Fábio Moreira
8e6adb1ef4 test: update snapshots 2018-05-16 17:06:21 +01:00
Fábio Moreira
a1154b2520 feat(ui-toolkit): improve parallax scroll behaviour 2018-05-16 17:06:21 +01:00
Fábio Moreira
155a065281 feat(ui-toolkit): update spacing guidelines 2018-05-16 17:06:21 +01:00
Fábio Moreira
f388e52549 fix(ui-toolkit): add margin to playground tab headers 2018-05-16 17:06:21 +01:00
Fábio Moreira
882085a170 feat(ui-toolkit): skip parallax header after user scrolls 2018-05-16 17:06:21 +01:00
Fábio Moreira
77fd895b6c feat(ui-toolkit): add parallax header after scroll 2018-05-16 17:06:21 +01:00
Fábio Moreira
bd332423be feat(ui-toolkit): define next section for empty link 2018-05-16 17:06:21 +01:00
Fábio Moreira
91e1fb192b feat(ui-toolkit): refator bottomNav to work for subSections 2018-05-16 17:06:21 +01:00
Fábio Moreira
88fe0ea92d feat(ui-toolkit): replace bottom nav arrows with html entities 2018-05-16 17:06:21 +01:00
Fábio Moreira
8bb4c31aba feat(ui-toolkit): add bottom nav to jump to previous or next session 2018-05-16 17:06:21 +01:00
Fábio Moreira
867e9b35a0 fix(instances): change packages text and link 2018-05-11 12:58:56 +01:00
Fábio Moreira
574c3add4e fix(ui-toolkit): remove horizontal scroll on styleguide #1384 2018-05-11 12:58:27 +01:00
Fábio Moreira
c233b0d757 fix(ui-toolkit): styleguide minor bugs 2018-05-11 12:58:27 +01:00
johnytiago
84b2d67d2c feat(instances): Adds Counts, Updates Firewall 2018-05-11 12:57:39 +01:00
johnytiago
062ff0ba67 feat(instances): status icon cross, cns tags color 2018-05-11 12:57:39 +01:00
Sérgio Ramos
b00baa7028 fix(instances): don't send cns names unless they exist 2018-05-09 14:24:07 +01:00
Fábio Moreira
588b833045 fix(ui-toolkit): replace styled card with card component on section 2018-05-09 12:17:20 +01:00
Sérgio Ramos
b3edb3aa02 fix(instances): handle name/identity aff props 2018-05-09 12:06:10 +01:00
Sérgio Ramos
542b491b52 refactor(instances): s/network/networks 2018-05-09 11:36:16 +01:00
Sérgio Ramos
7d8b478d20 test(instances): update snapshots 2018-05-09 11:29:17 +01:00
Joao Tiago
7531e503f2 fix(instances): Save -> Next, Create takes to name card (#1411) 2018-05-09 11:27:33 +01:00
Sérgio Ramos
d1b2ba0002 fix(instances): add Next button to fw 2018-05-09 11:22:27 +01:00
Sérgio Ramos
98a8b2eb5b fix(instances): send tags when creating instance 2018-05-09 11:21:40 +01:00
Sérgio Ramos
de356e1fbe fix(instances): send user-script when creating instance 2018-05-09 11:11:03 +01:00
Sérgio Ramos
f9c89cbccc fix(instances): send network ids when creating instance 2018-05-09 10:57:37 +01:00
johnytiago
1317894f27 feat(instances): wip networks, tags 2018-05-09 10:52:31 +01:00
Sérgio Ramos
f007889283 fix: try to serve a static route before maping assets 2018-05-09 10:34:49 +01:00
Sérgio Ramos
831be94521 build: fix ssr build 2018-05-09 00:03:57 +01:00
Sérgio Ramos
e321c20906 fix(instances): s/userScript/user-script 2018-05-08 18:02:01 +01:00
Sérgio Ramos
429ad17262 refactor(instances): better disabled/isValid handling 2018-05-08 17:55:06 +01:00
Sérgio Ramos
d591d93547 fix: prevent image card from growing to much 2018-05-08 16:55:18 +01:00
Sérgio Ramos
33d3d4343d fix: add margins to section-list/menu 2018-05-08 16:00:11 +01:00
johnytiago
32d5cdc293 refactor(instances): refactor create instance flow
fixes #1354
2018-05-08 15:32:10 +01:00
johnytiago
316773d9b7 feat(ui-toolkit): remove all margins
fixes #1156
2018-05-08 15:32:10 +01:00
Sérgio Ramos
0ae1290a5e feat: support ssr on bundle 2018-05-07 13:34:10 +01:00
Sérgio Ramos
3f75f812fa feat: add support for react-helmet 2018-05-07 12:09:12 +01:00
Fábio Moreira
25223e7324 feat(instances): update firewall text
fixes #1375
2018-05-04 14:50:47 +01:00
Fábio Moreira
7651dff619 feat(instances): update metadata link to docs
fixes  #1363
2018-05-04 12:25:18 +01:00
Sérgio Ramos
be18478f6c fix(ui-toolkit): disable LodashModuleReplacementPlugin in styleguide 2018-05-03 11:30:58 +01:00
geek
b929370124 fix: 404 on not found 2018-04-10 18:54:14 +01:00
Sérgio Ramos
e61399aa26 build(navigation): bump version 2018-04-10 15:56:17 +01:00
Sérgio Ramos
b429bc15ec fix(instances): add missing dependencies 2018-04-10 14:57:45 +01:00
Sérgio Ramos
1cd91b234f fix(instances): revert private 2018-04-09 19:08:32 +01:00
Sérgio Ramos
21317db964 fix(instances): add missing apollo-client dependency 2018-04-09 19:07:33 +01:00
Sérgio Ramos
95e420c419 feat(navigation): add support for account services 2018-04-06 16:10:15 +01:00
Sérgio Ramos
c413677b0e feat(instances): remove snapshots/package from instance list query 2018-04-05 18:00:09 +01:00
Sérgio Ramos
f73a9f4eca fix(bundle): handle namespaces 2018-04-05 17:58:53 +01:00
Sara Vieira
682f9db749 docs(ui-toolkit): add parallax
fixes #1225
2018-04-05 14:29:22 +01:00
Sara Vieira
5d274a419a feat(instances): add change name 2018-03-29 15:15:49 +01:00
Sérgio Ramos
0b9c464bb0 fix(navigation): correct logout href 2018-03-29 14:49:23 +01:00
Sara Vieira
e2a3fb73df feat(ui-toolkit): add principles 2018-03-29 13:50:43 +01:00
Sérgio Ramos
154b4cfeb6 fix(instances): support all fw rules 2018-03-28 16:32:09 +01:00
Sara Vieira
cac551beaf feat(navigation): add user dropdown 2018-03-28 15:31:40 +01:00
Sara Vieira
495af5702d feat(ui-toolkit): add header and sidebar 2018-03-28 14:42:39 +01:00
Sérgio Ramos
2accde1b4f fix(navigation): fix grid missing width in production 2018-03-27 16:43:55 +01:00
Sérgio Ramos
39d4e0b853 feat(navigation): no hardcoded datacenter 2018-03-27 15:39:46 +01:00
Sérgio Ramos
281f9ef60e feat(navigation): hide/show account depending on data 2018-03-26 20:35:10 +01:00
Wyatt Preul
b51f135703 fix(navigation): remove unused dep 2018-03-26 20:25:53 +01:00
geek
30b996fc48 fix: include cookie for nav requests 2018-03-26 20:21:53 +01:00
Sérgio Ramos
023e8242b7 test(instances): update snapshots 2018-03-26 20:18:37 +01:00
Sérgio Ramos
1298652193 fix: set correct protocol in ssr 2018-03-26 17:49:55 +01:00
Sara Vieira
86e60e6c5f build(navigation): integrate namespaces and fonts 2018-03-26 17:49:55 +01:00
Sérgio Ramos
76c6d18695 chore: change static logic to integrate with broader architecture 2018-03-23 18:20:54 +00:00
Sara Vieira
2937a37cdb docs(ui-toolkit): make toolkit render in separate pages 2018-03-23 17:46:42 +00:00
geek
35325cc573 fix: /{console}/graphql 2018-03-22 14:46:34 +00:00
Sara Vieira
7f0658e246 feat(instances): route objects id
fixes #1329
2018-03-21 17:35:51 +00:00
Sara Vieira
fab0bbfcc3 fix(instances): only use latest version of each image
fixes #1321
2018-03-21 16:19:17 +00:00
Sara Vieira
f5fbe0a169 fix(instances): make all prices 4 decimal points 2018-03-20 11:46:31 +00:00
Sara Vieira
9bdf02b1ba feat(ui-toolkit): add new components
references #1238

this includes:
 - add footer and frids
 - update colors
 - add tags
 - add empty table
 - add superscript
 - add toasts
2018-03-20 11:44:20 +00:00
Sara Vieira
55d5d24475 fix(ui-toolkit): don't grey out embedded selects
fixes #1273
2018-03-20 10:47:32 +00:00
Sara Vieira
95fc7092de feat(ui-toolkit): add popover button 2018-03-20 10:46:16 +00:00
Sérgio Ramos
63e7b31a4b feat(instances): instance list pagination 2018-03-16 20:19:37 +00:00
Sérgio Ramos
5f63a54ba5 feat(ui-toolkit): implement pagination components 2018-03-16 20:19:37 +00:00
Sérgio Ramos
1c967f1b9b feat(bundle): remove brok dependency 2018-03-16 20:19:37 +00:00
Sérgio Ramos
e95168bbf0 style: format 2018-03-15 19:05:02 +00:00
Sérgio Ramos
23ddc68ade style: global and simpler prettier config
this also removes precommit scripts because they were cumbersome
2018-03-15 19:02:55 +00:00
Sérgio Ramos
90d6b31264
feat(instances): remove not working aff features 2018-03-15 16:22:15 +00:00
Sérgio Ramos
2787f051e0 docs: document how to run the portal 2018-03-12 11:29:45 +00:00
Sérgio Ramos
d87c4e017b fix: namespace on chunks 2018-03-08 09:24:40 +00:00
Sérgio Ramos
d1fa4c7950 feat(navigation): sticky header 2018-03-08 09:24:40 +00:00
Sérgio Ramos
da59d5e808 feat: same-domain consoles 2018-03-08 09:24:40 +00:00
Wyatt Preul
b64dbb289d feat: set x-csrf-token header for requests 2018-03-06 10:39:34 +00:00
Sérgio Ramos
4a7956782b fix(instances): send the correct userscript to deploy 2018-03-02 15:42:29 +00:00
Sérgio Ramos
9eabff8a60 feat(instances): link user-scripts to metadata 2018-03-02 14:23:08 +00:00
Sérgio Ramos
4cf50a45c5 fix(instances): prompt to mutate instance on summary 2018-03-02 14:18:11 +00:00
Sérgio Ramos
81c119a066 fix(instances): set correct values when sorting packages 2018-03-02 14:11:02 +00:00
Sérgio Ramos
ec2f9fc141 fix(navigation): hide products-services 2018-03-02 14:10:32 +00:00
Sérgio Ramos
b6e7af2422 fix(instances): always get id when fetching resources 2018-03-02 14:09:17 +00:00
Sérgio Ramos
50faa9d9ac fix(my-joy-instances): truncate summary copy on Instance summary card
fixes #1296
2018-03-02 12:25:08 +00:00
Sérgio Ramos
3579c1d8f5 fix(my-joy-instances): fix breadcrumb routing to align with designs
fixes #1294
2018-03-02 12:21:46 +00:00
Sara Vieira
c673822cad feat(instances): show networks empty box 2018-03-02 12:16:37 +00:00
Sérgio Ramos
11c41272d0 test(instances): update snapshots 2018-03-02 12:10:18 +00:00
Sérgio Ramos
cb0b613034 fix(instances): fix fw submit value 2018-03-02 12:01:39 +00:00
Sérgio Ramos
ae866b401e fix(instances): handle Image label click 2018-03-02 11:52:46 +00:00
Sérgio Ramos
110c80f65f chore(instances): remove resize file 2018-03-02 11:52:28 +00:00
Sérgio Ramos
7f136cc8bc fix(instances): handle machines pagination in networks view 2018-03-02 11:49:16 +00:00
Sérgio Ramos
31b46b216f fix(instances): s/snapshots/instances in instances prompt 2018-03-02 11:48:55 +00:00
Sérgio Ramos
3769e0b5ba fix(instances): fix affinity rule parse 2018-03-02 11:41:42 +00:00
Sérgio Ramos
bf0a1a0ba0 feat: upgrade graphql with dc and pagination 2018-03-02 10:23:22 +00:00
Sérgio Ramos
c6b245aebc fix(instances): validation improvements
fixes #1292
2018-03-01 20:06:36 +00:00
Sérgio Ramos
d7f83c59fa feat: constants in IC (phase 1) 2018-03-01 17:23:03 +00:00
Sérgio Ramos
cd242d7505 feat: improved validation of attrs 2018-03-01 17:23:03 +00:00
Sara Vieira
9d10a3fa92 feat(my-joy-instances): simpler user scripts editor (#1289)
fixes #1187
2018-03-01 13:47:30 +00:00
Sérgio Ramos
abe7a58e3f
fix(images): filter images and tags
fixes #1285
2018-02-28 13:14:17 +00:00
Sérgio Ramos
577df6d187 fix(instances): search only by visible attrs 2018-02-28 12:34:36 +00:00
Sérgio Ramos
ce232b7d6a chore: rename navigation 2018-02-28 11:22:52 +00:00
Sérgio Ramos
e7cd8f7561 chore: cleanup deps and flows 2018-02-28 11:18:40 +00:00
Sérgio Ramos
33d6b2ecab fix(my-joy-beta): vms = true -> HVM 2018-02-27 15:23:29 +00:00
Sérgio Ramos
ac404ee33b fix(my-joy-beta): revert pkg selector in create-instance 2018-02-27 15:16:52 +00:00
Sérgio Ramos
584c8b2028 fix(my-joy-beta): mark optional sections as proceeded onEdit 2018-02-27 15:08:45 +00:00
Sara Vieira
25c2735bf4 fix(my-joy-beta): create instance package table alignments
fixes #1271
2018-02-27 14:40:35 +00:00
Sérgio Ramos
461d8697ca fix(my-joy-beta): fix tags/fw match
fixes #1267
2018-02-27 14:26:44 +00:00
Sara Vieira
d8618eab30 fix(my-joy-beta): copy tweaks
fixes #1275
2018-02-27 14:26:44 +00:00
Sara Vieira
255fd67f48 fix(my-joy-beta): fis badge position 2018-02-27 14:23:11 +00:00
Sara Vieira
6848302022 fix(my-joy-beta): copy tweaks
fixes #1275
2018-02-27 13:56:34 +00:00
Sara Vieira
943ef1daf6 fix(ui-toolkit): remove meta if no message 2018-02-27 13:41:36 +00:00
Sara Vieira
84f6faf693 fix(my-joy-beta): fix section-list scrolling
fixes #1266
2018-02-27 13:24:36 +00:00
Sérgio Ramos
e405d47cea fix(my-joy-beta): re-enable navbar 2018-02-27 11:51:41 +00:00
Sérgio Ramos
2b2e431830 fix(bundle): explicit keyId param to Api 2018-02-27 11:41:40 +00:00
Sérgio Ramos
e540425472 fix: require theme from toolkit, not state 2018-02-27 10:59:15 +00:00
Sérgio Ramos
a6b32631c4 fix(my-joy-beta): show snapshots mutation name 2018-02-26 19:45:07 +00:00
Sérgio Ramos
8b67cae8f3 test(my-joy-beta): fix failed merge 2018-02-26 19:13:09 +00:00
Sara Vieira
21c0bc70a3 fix(my-joy-beta): fix small bugs
fixes #1259
fixes #1255
2018-02-26 18:45:40 +00:00
Sérgio Ramos
1a8a91e41e feat(bundle): update cloudapi-gql dependency 2018-02-26 17:52:43 +00:00
Sara Vieira
99374a8170 feat(ui-toolkit): replace bold with semibold fonts 2018-02-26 15:54:25 +00:00
Sérgio Ramos
71c7743fef revert(my-joy-beta): use an older joyent-manifest-editor version 2018-02-26 15:20:56 +00:00
Sérgio Ramos
311dc6361b test(my-joy-beta): update snapshots 2018-02-26 14:43:16 +00:00
Sérgio Ramos
de5e7e9663 fix(my-joy-beta): show top border on <Empty /> when not in list 2018-02-26 14:43:16 +00:00
Sérgio Ramos
486fd8664f fix(my-joy-beta): small create image button (tmp) 2018-02-26 14:43:16 +00:00
Sérgio Ramos
b72714fc94 style(my-joy-beta): lint 2018-02-26 14:43:16 +00:00
Sérgio Ramos
5d333c57a6 revert(ui-tookit): messages full-width and no tip 2018-02-26 14:43:16 +00:00
Sara Vieira
8b36f40e9d feat(my-joy-beta): new image tabs on instance creation 2018-02-26 14:23:06 +00:00
Sara Vieira
5bd261ad6b fix(ui-toolkit): overhaul components
fixes #1226
2018-02-26 12:07:30 +00:00
Sérgio Ramos
f8675e4d8d feat(my-joy-beta): show public/private label in ips 2018-02-26 11:34:28 +00:00
Sérgio Ramos
318a510ee8 build(my-joy-beta): initial ssr support
fixes #1247
2018-02-26 10:16:17 +00:00
Sérgio Ramos
05550dd570 fix(my-joy-beta): use redux-form to handle package selection (revert) 2018-02-23 20:02:17 +00:00
Sara Vieira
7d618d9992 feat(my-joy-beta): add validation
fixes #1244
2018-02-23 17:57:13 +00:00
Sara Vieira
9e67c3ab67 fix(my-joy-beta): fix multiple bugs
fixes #1208
2018-02-23 15:59:12 +00:00
Sérgio Ramos
ef61bbce61 test(my-joy-beta): update snapshots 2018-02-22 02:22:00 +00:00
Sérgio Ramos
d13e23e15d fix(my-joy-beta): make logo click trigger change 2018-02-22 02:08:18 +00:00
Sérgio Ramos
4a8fa480c4 fix(my-joy-beta): remove table header bottom border 2018-02-22 02:08:03 +00:00
Sérgio Ramos
c7043f0b3a lint(my-joy-beta): remove unused var 2018-02-22 02:07:24 +00:00
Sérgio Ramos
6f36e62f92 chore: update snapshots
# Conflicts:
#	packages/my-joy-beta/src/components/__tests__/__image_snapshots__/key-value-ui-js-key-value-submitting-1-snap.png
#	packages/my-joy-beta/src/components/__tests__/__snapshots__/key-value.spec.js.snap
#	packages/my-joy-beta/src/components/create-instance/__tests__/__image_snapshots__/images-ui-js-images-images-name-stuff-image-name-stuff-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/snapshots-ui-js-item-mutating-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-instance-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-instance-2-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-starting-stopping-rebooting-removing-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-state-provisioning-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-state-running-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__image_snapshots__/summary-ui-js-summary-state-stopped-1-snap.png
#	packages/my-joy-beta/src/components/instances/__tests__/__snapshots__/metadata.spec.js.snap
#	packages/my-joy-beta/src/components/instances/__tests__/__snapshots__/summary.spec.js.snap
#	packages/my-joy-beta/src/containers/create-instance/__tests__/__snapshots__/metadata.spec.js.snap
#	packages/my-joy-beta/src/containers/create-instance/__tests__/__snapshots__/user-script.spec.js.snap
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/cns-ui-js-cns-loading-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/firewall-ui-js-firewall-loading-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/list-ui-js-list-loading-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/metadata-ui-js-metadata-metadata-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/networks-ui-js-networks-loading-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/summary-ui-js-summary-loading-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/summary-ui-js-summary-starting-stopping-rebooting-removing-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/summary-ui-js-summary-starting-stopping-rebooting-removing-2-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/summary-ui-js-summary-starting-stopping-rebooting-removing-3-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__image_snapshots__/tags-ui-js-tags-editing-removing-1-snap.png
#	packages/my-joy-beta/src/containers/instances/__tests__/__snapshots__/metadata.spec.js.snap
#	packages/my-joy-beta/src/containers/instances/__tests__/__snapshots__/summary.spec.js.snap
2018-02-22 01:22:27 +00:00
Sérgio Ramos
486d6f54f4 fix(my-joy-beta): 12px vertical-vidiver margin in summary 2018-02-22 01:04:57 +00:00
Sérgio Ramos
b5a29eb300 fix(my-joy-beta): s/Snapshot key/Snapshot name 2018-02-22 01:04:57 +00:00
Sérgio Ramos
ffe9d2321c fix(my-joy-beta): show instance name instead of package in summary 2018-02-22 01:04:57 +00:00
Sérgio Ramos
50c1b6e9b1 fix(my-joy-beta): fix affinity field sizes
# Conflicts:
#	packages/my-joy-beta/src/containers/create-instance/index.js
2018-02-22 01:04:50 +00:00
Sérgio Ramos
01e10c6962 fix(my-joy-beta): show create instance errors
# Conflicts:
#	packages/my-joy-beta/src/containers/create-instance/index.js
2018-02-22 01:03:37 +00:00
Sérgio Ramos
9d7e4e22d5 fix(my-joy-beta): fix pkg vcpus sort 2018-02-22 01:01:08 +00:00
Sérgio Ramos
3bfc7bd14b fix(my-joy-beta): validate tags and metadata 2018-02-22 01:01:08 +00:00
Sérgio Ramos
4593980883 feat(my-joy-beta): confirm some mutations 2018-02-22 01:01:08 +00:00
Sérgio Ramos
929fcf0ab4 fix(my-joy-beta): don't require proceeded to deploy
# Conflicts:
#	packages/my-joy-beta/src/containers/create-instance/index.js
#	packages/my-joy-beta/src/containers/create-instance/tags.js
#	yarn.lock
2018-02-22 01:01:01 +00:00
Sérgio Ramos
234a481c32 fix(my-joy-beta): remove weird char from metadata 2018-02-22 00:56:54 +00:00
Sérgio Ramos
a9c63c20a7 fix(my-joy-beta): update pkg list to match image type 2018-02-22 00:56:48 +00:00
Sérgio Ramos
17f58e4089 fix(my-joy-beta): credentials-same-origin on apollo-client
# Conflicts:
#	packages/my-joy-beta/package.json
2018-02-22 00:56:48 +00:00
Sérgio Ramos
cb9ee4644e build: build on install 2018-02-22 00:56:14 +00:00
Sérgio Ramos
9966143337 build: build on install 2018-02-19 15:00:54 +00:00
Sérgio Ramos
d879202b4e feat: create instance from image, and vice-versa 2018-02-16 19:12:50 +00:00
Sérgio Ramos
bf9a85e4fd feat(images): mutate tags 2018-02-16 19:12:50 +00:00
Sérgio Ramos
eae5345e62 chore: bump majors 2018-02-16 18:32:15 +00:00
Sérgio Ramos
a7ca59e4f2 fix(my-joy-beta): consisten onClick on create-instance headers 2018-02-16 18:22:31 +00:00
Sérgio Ramos
a1423ea53f fix(my-joy-beta): don't sort networks by selected 2018-02-16 18:22:31 +00:00
Sérgio Ramos
11b9f1978a fix(my-joy-beta): fix latest image selection 2018-02-16 18:22:31 +00:00
Sérgio Ramos
a5ea102ab3 chore(ui-toolkit): update react-bundle 2018-02-16 18:22:31 +00:00
Sérgio Ramos
99ceda9033 fix: add missing dependencies 2018-02-16 18:22:31 +00:00
Sara Vieira
a5eb142dcc fix(my-joy-beta): fix multiple small issues 2018-02-16 18:22:31 +00:00
Sara Vieira
bf5f0463e7 feat(images): delete image in summary and list
fixes #1204
2018-02-14 19:36:31 +00:00
Sérgio Ramos
a7283454b8 fix: add missing dependencies 2018-02-14 17:20:19 +00:00
Sara Vieira
e7a92656d6 fix(my-joy-beta): fix multiple small issues 2018-02-14 16:09:24 +00:00
Sara Vieira
4dc7b9179b fix(images): fix small bugs from QA
fixes #1202
2018-02-13 20:33:03 +00:00
Sara Vieira
29fc12d4dd feat(images): create image 2018-02-13 20:03:57 +00:00
Sara Vieira
ca1236fb80
fix(my-joy-beta): change menu name to cns (#1178) 2018-02-12 10:43:32 +00:00
Sara Vieira
1a53b0753c feat(images): prettify list 2018-02-09 14:16:42 +00:00
Sérgio Ramos
36756fe75b feat(images): bootstrap 2018-02-09 14:16:42 +00:00
Sara Vieira
80af7af938 fix(my-joy-beta): fix multiple small issues 2018-02-09 13:14:19 +00:00
Sara Vieira
533e7d875e
chore(my-joy-beta): add mixpanel 2018-02-06 16:02:37 +00:00
Sara Vieira
23e4554bf2 feat(ui-toolkit): add copiable field 2018-02-06 11:27:10 +00:00
Sara Vieira
de5fba81e3 feat(my-joy-beta): add empty state to instance list
fixes #765
2018-02-05 14:18:30 +00:00
Sara Vieira
5eff7e0883 fix(my-joy-beta): animation speed based on component height
fixes #1137
2018-02-05 13:50:43 +00:00
1160 changed files with 106698 additions and 117983 deletions

View File

@ -1,165 +0,0 @@
### Bower ###
bower_components
.bower-cache
.bower-registry
.bower-tmp
### Git ###
*.orig
### macOS ###
*.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### Vim ###
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Application Specific ###
.env
# PostCSS
*.postcss.js
/cloudapi-graphql/credentials.json
/docker-graphql/config.json
tap-xunit
/ui/dist
_todo
packages/*/dist
packages/*/buid
packages/ui-toolkit/styleguide
packages/*/node_modules
packages/*/Dockerfile
legacy
_env

View File

@ -1,4 +1,9 @@
packages/*/**
prototypes/*/**
artifacts
reports
.nyc_output
coverage
dist
styleguide
build
consoles/*/lib/app
node_modules

View File

@ -1,8 +1,10 @@
{
"extends": "joyent-portal",
"rules": {
"jsx-a11y/href-no-hash": 0,
"no-console": 1,
"new-cap": 0,
"no-console": 0
"jsx-a11y/href-no-hash": 0,
"no-negated-condition": 1,
"camelcase": 1
}
}

View File

@ -17,9 +17,9 @@ perf
refactor
revert
style
test
test
```
And where scope is one of ui-toolkit, my-joy-beta, cloudapi-gql, boilerplate, and create-instance.
*The recommended method to commit should be by running npm run commit.*
_The recommended method to commit should be by running npm run commit._

View File

@ -1,27 +1,17 @@
## I'm submitting a...
- [ ] bug report
- [ ] feature request
- [ ] design request
* [ ] bug report
* [ ] feature request
* [ ] design request
## What is the current behavior?
## If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem
## What is the expected behavior?
## What is the motivation / use case for changing the behavior?
## If the current behavior is a bug, please provide your browser
## Other information

View File

@ -1,22 +1,14 @@
**Please check if the PR fulfills these requirements**
- [ ] The commit message follows our [guidelines](https://github.com/yldio/joyent-portal/blob/master/.github/COMMIT_GUIDELINES.md)
- [ ] Tests for the changes have been added (for bug fixes / features)
* [ ] The commit message follows our [guidelines](https://github.com/yldio/joyent-portal/blob/master/.github/COMMIT_GUIDELINES.md)
* [ ] Tests for the changes have been added (for bug fixes / features)
**What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
**Does this PR close an issue?** (If not please create one)
**What is the new behavior (if this is a feature change)?**
**Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?)
**Other information**

2
.gitignore vendored
View File

@ -165,3 +165,5 @@ prototypes/*/package-lock.json
_env*
keys*
/packages/*/public/index.html
/consoles/*/public/index.html

View File

@ -1,11 +1,14 @@
{
"ignoreDevDependencies": true,
"allowedPackages": [{
"name": "colors",
"extraFieldsForDocumentation": "Licence is MIT, but was not found by tool: https://github.com/Marak/colors.js/blob/v0.5.1/MIT-LICENSE.txt",
"date": "17 January 2017",
"reason": "MIT Licenced"
}],
"allowedPackages": [
{
"name": "colors",
"extraFieldsForDocumentation":
"Licence is MIT, but was not found by tool: https://github.com/Marak/colors.js/blob/v0.5.1/MIT-LICENSE.txt",
"date": "17 January 2017",
"reason": "MIT Licenced"
}
],
"allowedLicenses": [
"CC-BY-4.0",
"CC0-1.0",

29
.prettierignore Normal file
View File

@ -0,0 +1,29 @@
.git/*
.DS_Store
license
yarn.lock
.travis.yml
.yarnclean
.eslintignore
.prettierignore
.npmignore
.gitignore
.dockerignore
dist
build
packages/*/lib/app
consoles/*/lib/app
*.ico
*.html
*.log
*.svg
*.map
*.png
*.snap
*.ttf
*.sh
*.txt

31
.prettierrc Normal file
View File

@ -0,0 +1,31 @@
{
"bracketSpacing": true,
"jsxBracketSameLine": false,
"printWidth": 80,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false,
"overrides": [
{
"files": [
".prettierrc",
".eslintrc",
".babelrc",
".tern-project",
".stylelintrc",
".lighthouserc"
],
"options": {
"parser": "json"
}
},
{
"files": ["package.json"],
"options": {
"printWidth": 180
}
}
]
}

View File

@ -1,8 +1,5 @@
{
"libs": [
"ecmascript",
"browser"
],
"libs": ["ecmascript", "browser"],
"plugins": {
"doc_comment": true,
"local-scope": true,
@ -12,4 +9,4 @@
"configPath": "./webpack/index.js"
}
}
}
}

View File

@ -1,5 +1,5 @@
language: node_js
node_js:
- '8'
- '9'
script:
- npm run test-ci
- yarn run test:ci

28
.vscode/launch.json vendored
View File

@ -1,15 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3069/",
"webRoot": "${workspaceRoot}"
}
]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3069/",
"webRoot": "${workspaceRoot}"
}
]
}

1
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1 @@
{}

8
bundle/.env.js Normal file
View File

@ -0,0 +1,8 @@
'use strict';
const { homedir } = require('os');
const { join } = require('path');
const { SDC_KEY_PATH } = process.env;
process.env.SDC_KEY_PATH = SDC_KEY_PATH || join(homedir(), './.ssh/id_rsa');

20
bundle/data/account.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = [
{
name: 'Logout',
slug: 'logout',
description: 'Do the daggum logout',
url: '/logout'
},
{
name: 'Change Password',
slug: 'change-password',
description: 'Change yer own password',
url: '/password'
},
{
name: 'Account',
slug: 'account',
description: 'Your account information',
url: '/account'
}
];

89
bundle/data/categories.js Normal file
View File

@ -0,0 +1,89 @@
module.exports = [
{
name: 'Compute',
services: [
{
name: 'VMs & Containers',
slug: 'instances',
description: 'Run VMs and bare metal containers'
}
]
},
{
name: 'Network',
services: [
{
name: 'VLANs',
slug: 'vlans',
description: 'Wire your application your way'
},
{
name: 'Subnets',
slug: 'subnets',
description: 'A network for everything'
},
{
name: 'Firewall Rules',
slug: 'firewall',
description: 'Control the bits coming and going'
}
]
},
{
name: 'Storage',
services: [
{
name: 'Triton Object Storage',
slug: 'object-storage',
description: 'Modern cloud object storage',
tags: ["'note'='was Manta'"]
},
{
name: 'S3 Compatibility Bridge',
slug: 's3-bridge',
description: 'Modern storage, legacy compability'
},
{
name: 'Triton Volumes',
slug: 'volumes',
description: 'Network filesystems for your apps',
tags: ["'is-new'='true'"]
}
]
},
{
name: 'Access',
services: [
{
name: 'Role Based Access Control',
slug: 'rbac',
description: 'Manage users within your account'
}
]
},
{
name: 'Help & Support',
services: [
{
name: 'Service Status',
slug: 'status',
description: 'Find out about the status of our services'
},
{
name: 'Contact Support',
slug: 'contact-support',
description: 'Chat to us via phone or email'
},
{
name: 'Support Plans',
slug: 'support-plans',
description: 'Write here about Support Plans'
},
{
name: 'Getting Started',
slug: 'getting-started',
description: 'Write here about Getting Started'
}
]
}
];

86
bundle/data/regions.js Normal file
View File

@ -0,0 +1,86 @@
module.exports = [
{
name: 'Ashburn, Virginia, USA',
continent: 'NORTH_AMERICA',
datacenters: [
{
name: 'us-east-1',
url: 'http://localhost'
},
{
name: 'us-east-2',
url: 'http://localhost'
},
{
name: 'us-east-3',
url: 'http://localhost'
}
]
},
{
name: 'Las Vegas, Nevada, USA',
continent: 'NORTH_AMERICA',
datacenters: [
{
name: 'us-sw-1',
url: 'http://localhost'
}
]
},
{
name: 'Emeryville, California, USA',
continent: 'NORTH_AMERICA',
datacenters: [
{
name: 'us-west-1',
url: 'http://localhost'
}
]
},
{
name: 'Amsterdam, Netherlands',
continent: 'EUROPE',
datacenters: [
{
name: 'us-ams-1',
url: 'http://localhost'
}
]
},
{
name: 'Singapore',
continent: 'ASIA',
datacenters: [
{
name: 'ap-sg-1',
url: 'http://localhost'
},
{
name: 'ap-sg-2',
url: 'http://localhost'
},
{
name: 'ap-sg-3',
url: 'http://localhost'
}
]
},
{
name: 'Seoul, South Korea',
continent: 'ASIA',
datacenters: [
{
name: 'ap-kr-1',
url: 'http://localhost'
},
{
name: 'ap-kr-2',
url: 'http://localhost'
},
{
name: 'ap-kr-3',
url: 'http://localhost'
}
]
}
];

View File

@ -1,85 +1,72 @@
// Requires .env.js file with the following exports:
// SDC_URL, SDC_KEY_ID, SDC_KEY_PATH
require('./.env.js');
const Brule = require('brule');
const Hapi = require('hapi');
const Inert = require('inert');
const Main = require('apr-main');
const Rollover = require('rollover');
const Hapi = require('hapi');
const H2O2 = require('h2o2');
const Execa = require('execa');
const Path = require('path');
const Fs = require('fs');
const Sso = require('minio-proto-auth');
const Ui = require('my-joy-beta');
const Nav = require('joyent-navigation');
const Api = require('cloudapi-gql');
const { PORT = 4000 } = process.env;
const ROOT = Path.join(__dirname, 'src');
const {
PORT = 3069,
COOKIE_PASSWORD,
COOKIE_DOMAIN,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID,
SDC_URL,
BASE_URL = `http://0.0.0.0:${PORT}`,
ROLLBAR_SERVER_TOKEN,
NODE_ENV = 'development'
} = process.env;
const calcPort = i => Number(PORT) + Number(i) + 1;
const server = Hapi.server({
port: PORT,
host: '127.0.0.1'
const namespaces = Fs.readdirSync(ROOT)
.filter(filename => /.js$/.test(filename))
.map(filename => filename.replace(/.js$/, ''))
.filter(filename => !['index', 'server'].includes(filename));
const routes = namespaces.map((namespace, i) => ({
method: '*',
path: `/${namespace}/{params*}`,
handler: {
proxy: {
uri: `{protocol}://0.0.0.0:${calcPort(i)}/${namespace}/{params}`
}
}
}));
namespaces.forEach((namespace, i) => {
const child = Execa('node', [namespace], {
cwd: ROOT,
cleanup: true,
env: Object.assign({}, process.env, {
PORT: calcPort(i),
PREFIX: namespace
})
});
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
});
Main(async () => {
await server.register([
// {
// plugin: Rollover,
// options: {
// rollbar: {
// accessToken: ROLLBAR_SERVER_TOKEN,
// reportLevel: 'error'
// }
// }
// },
{
plugin: Brule,
options: {
auth: false
const server = Hapi.server({
port: PORT,
routes: {
cors: {
origin: ['*'],
credentials: true,
additionalHeaders: ['Cookie', 'X-CSRF-Token']
}
},
{
plugin: Sso,
options: {
cookie: {
password: COOKIE_PASSWORD,
domain: COOKIE_DOMAIN,
isSecure: false,
isHttpOnly: true,
ttl: 1000 * 60 * 60 // 1 hour
},
sso: {
keyPath: SDC_KEY_PATH,
keyId: '/' + SDC_ACCOUNT + '/keys/' + SDC_KEY_ID,
apiBaseUrl: SDC_URL,
url: 'https://sso.joyent.com/login',
permissions: { cloudapi: ['/my/*'] },
baseUrl: BASE_URL,
isDev: NODE_ENV === 'development'
}
}
},
{
plugin: Nav
},
{
plugin: Ui
},
{
plugin: Api
debug: {
log: ['error'],
request: ['error']
}
]);
});
server.auth.default('sso');
await server.register({
plugin: H2O2
});
routes.map(route => server.route(route));
await server.start();
console.log(`server started at http://localhost:${server.info.port}`);
// eslint-disable-next-line no-console
console.log(`server started at http://0.0.0.0:${server.info.port}`);
});

View File

@ -4,22 +4,28 @@
"private": true,
"license": "MPL-2.0",
"scripts": {
"start": "NODE_ENV=development PORT=3069 REACT_APP_GQL_PORT=3069 REACT_APP_GQL_PROTOCOL=http node -r ./_env.js index.js",
"lint-ci": "echo 0",
"lint": "echo 0",
"test-ci": "echo 0",
"dev": "NODE_ENV=development PORT=4000 node index.js",
"build:test": "echo 0",
"build:lib": "echo 0",
"build:bundle": "echo 0",
"prepublish": "echo 0",
"test": "echo 0",
"prepublish": "echo 0"
"test:ci": "echo 0"
},
"dependencies": {
"apr-main": "^4.0.3",
"brule": "^3.1.0",
"cloudapi-gql": "^4.5.1",
"hapi": "^17.2.0",
"inert": "^5.1.0",
"joyent-navigation": "^1.0.0",
"minio-proto-auth": "^1.1.0",
"my-joy-beta": "^1.0.0",
"rollover": "^1.0.0"
"cloudapi-gql": "^8.0.0",
"execa": "^0.10.0",
"graphi": "^5.7.0",
"h2o2": "^8.1.2",
"hapi": "^17.5.0",
"hapi-triton-auth": "^3.0.0",
"hapi-webconsole-nav": "^2.1.0",
"my-joy-images": "*",
"my-joy-instances": "*",
"my-joy-navigation": "*",
"my-joy-service-groups": "*",
"my-joy-templates": "*",
"tsg-graphql": "^1.0.0"
}
}

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e -o pipefail
TRITON_ACCOUNT=$(triton account get | awk -F": " '/id:/{print $2}')
TRITON_DC=$(triton profile get | awk -F"/" '/url:/{print $3}' | awk -F'.' '{print $1}')
DEFAULT_DOMAIN=${TRITON_ACCOUNT}.${TRITON_DC}.cns.triton.zone
read -p "Enter the domain name you plan to use for this key [$DEFAULT_DOMAIN]: " domain
domain="${domain:-$DEFAULT_DOMAIN}"
echo -n "Enter the password to use for the key: "
read -s password
echo
echo "Generating key for $domain"
keys_path=keys-$domain
mkdir -p $keys_path
openssl genrsa -aes256 -passout pass:$password -out $keys_path/ca.key 4096
chmod 400 $keys_path/ca.key
openssl req -new -x509 -sha256 -days 730 -key $keys_path/ca.key -out $keys_path/ca.crt -passin pass:$password -subj "/CN=copilot"
chmod 444 $keys_path/ca.crt
openssl genrsa -out $keys_path/server.key 2048
chmod 400 $keys_path/server.key
openssl req -new -key $keys_path/server.key -sha256 -out $keys_path/server.csr -passin pass:$password -subj "/CN=$domain"
openssl x509 -req -days 365 -sha256 -in $keys_path/server.csr -passin pass:$password -CA $keys_path/ca.crt -CAkey $keys_path/ca.key -set_serial 1 -out $keys_path/server.crt
chmod 444 $keys_path/server.crt
openssl genrsa -out $keys_path/client.key 2048
openssl req -new -key $keys_path/client.key -out $keys_path/client.csr -subj "/CN=$domain"
openssl x509 -req -days 365 -sha256 -in $keys_path/client.csr -CA $keys_path/ca.crt -CAkey $keys_path/ca.key -set_serial 2 -out $keys_path/client.crt -passin pass:$password
openssl pkcs12 -export -clcerts -in $keys_path/client.crt -inkey $keys_path/client.key -out $keys_path/client.p12 -passout pass:$password
# open $keys_path/client.p12 &
echo
echo "You can complete setup by running './setup.sh ~/path/to/TRITON_PRIVATE_KEY $keys_path/ca.crt $keys_path/server.key $keys_path/server.crt'"

View File

@ -1,235 +0,0 @@
#!/bin/bash
set -e -o pipefail
help() {
echo
echo 'Usage ./setup.sh ~/path/to/TRITON_PRIVATE_KEY ~/path/to/CA_CRT ~/path/to/SERVER_KEY ~/path/to/SERVER_CRT'
echo
echo 'Checks that your Triton and Docker environment is sane and configures'
echo 'an environment file to use.'
echo
echo 'TRITON_PRIVATE_KEY is the filesystem path to an SSH private key'
echo 'used to connect to Triton.'
echo
echo 'CA_CRT is the filesystem path to a certificate authority crt file.'
echo
echo 'SERVER_KEY is the filesystem path to a TLS server key file.'
echo
echo 'SERVER_CRT is the filesystem path to a TLS server crt file.'
echo
}
# Check for correct configuration
check() {
if [ -z "$1" ]; then
tput rev # reverse
tput bold # bold
echo 'Please provide a path to a SSH private key to access Triton.'
tput sgr0 # clear
help
exit 1
fi
if [ ! -f "$1" ]; then
tput rev # reverse
tput bold # bold
echo 'SSH private key for Triton is unreadable.'
tput sgr0 # clear
help
exit 1
fi
# Assign args to named vars
TRITON_PRIVATE_KEY_PATH=$1
if [ -z "$2" ]; then
tput rev # reverse
tput bold # bold
echo 'Please provide a path to the NGINX CA crt file.'
tput sgr0 # clear
help
exit 1
fi
if [ ! -f "$2" ]; then
tput rev # reverse
tput bold # bold
echo 'CA certificate for NGINX is unreadable.'
tput sgr0 # clear
help
exit 1
fi
NGINX_CA_CRT_PATH=$2
if [ -z "$3" ]; then
tput rev # reverse
tput bold # bold
echo 'Please provide a path to the server key file.'
tput sgr0 # clear
help
exit 1
fi
if [ ! -f "$3" ]; then
tput rev # reverse
tput bold # bold
echo 'Server key file for NGINX is unreadable.'
tput sgr0 # clear
help
exit 1
fi
NGINX_SERVER_KEY_PATH=$3
if [ -z "$4" ]; then
tput rev # reverse
tput bold # bold
echo 'Please provide a path to the server crt file.'
tput sgr0 # clear
help
exit 1
fi
if [ ! -f "$4" ]; then
tput rev # reverse
tput bold # bold
echo 'Server crt file for NGINX is unreadable.'
tput sgr0 # clear
help
exit 1
fi
NGINX_SERVER_CRT_PATH=$4
command -v docker >/dev/null 2>&1 || {
echo
tput rev # reverse
tput bold # bold
echo 'Docker is required, but does not appear to be installed.'
tput sgr0 # clear
echo 'See https://docs.joyent.com/public-cloud/api-access/docker'
exit 1
}
command -v triton >/dev/null 2>&1 || {
echo
tput rev # reverse
tput bold # bold
echo 'Error! Joyent Triton CLI is required, but does not appear to be installed.'
tput sgr0 # clear
echo 'See https://www.joyent.com/blog/introducing-the-triton-command-line-tool'
exit 1
}
TRITON_USER=$(triton profile get | awk -F": " '/account:/{print $2}')
TRITON_DC=$(triton profile get | awk -F"/" '/url:/{print $3}' | awk -F'.' '{print $1}')
TRITON_ACCOUNT=$(triton account get | awk -F": " '/id:/{print $2}')
SDC_URL=$(triton env | grep SDC_URL | awk -F"=" '{print $2}' | awk -F"\"" '{print $2}')
SDC_ACCOUNT=$(triton env | grep SDC_ACCOUNT | awk -F"=" '{print $2}' | awk -F"\"" '{print $2}')
SDC_KEY_ID=$(triton env | grep SDC_KEY_ID | awk -F"=" '{print $2}' | awk -F"\"" '{print $2}')
DOCKER_CERT_PATH=$(triton env | grep DOCKER_CERT_PATH | awk -F"=" '{print $2}')
DOCKER_HOST=$(triton env | grep DOCKER_HOST | awk -F"=" '{print $2}')
rm _env_consul
rm _env_mysql
rm _env
echo MYSQL_DATABASE=bridge-db >> _env_mysql
echo 'MYSQL_ROOT_PASSWORD='$(cat /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12) >> _env_mysql
echo MYSQL_USER=bridge-user >> _env_mysql
echo 'MYSQL_PASSWORD='$(cat /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 8) >> _env_mysql
echo >> _env_mysql
echo '# Consul discovery via Triton CNS' >> _env_consul
echo CONSUL=bridge-consul.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.joyent.com >> _env_consul
echo CONSUL_AGENT=1 >> _env_consul
echo >> _env_consul
TRITON_CREDS_PATH=/root/.triton
echo '# Allowed list of account Ids who can access the site' >> _env
echo ALLOWED_ACCOUNTS=${TRITON_ACCOUNT} >> _env
echo >> _env
echo '# Site URL' >> _env
echo BASE_URL=https://bridge.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.triton.zone >> _env
echo COOKIE_DOMAIN=triton.zone >> _env
echo >> _env
echo '# MySQL via Triton CNS' >> _env
echo MYSQL_HOST=bridge-mysql.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.joyent.com >> _env
echo >> _env
echo PORT=8080 >> _env
echo 'COOKIE_PASSWORD='$(cat /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 36) >> _env
echo SDC_KEY_PATH=/root/.ssh/id_rsa >> _env
echo DOCKER_CERT_PATH=${TRITON_CREDS_PATH} >> _env
echo TRITON_CREDS_PATH=${TRITON_CREDS_PATH} >> _env
echo DOCKER_TLS_VERIFY=1 >> _env
echo DOCKER_HOST=${DOCKER_HOST} >> _env
echo SDC_URL=${SDC_URL} >> _env
echo SDC_ACCOUNT=${SDC_ACCOUNT} >> _env
echo SDC_KEY_ID=${SDC_KEY_ID} >> _env
echo CONSUL=bridge-consul.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.joyent.com >> _env
echo TRITON_CA=$(cat "${DOCKER_CERT_PATH}"/ca.pem | tr '\n' '#') >> _env
echo TRITON_CA_PATH=${TRITON_CREDS_PATH}/ca.pem >> _env
echo TRITON_KEY=$(cat "${DOCKER_CERT_PATH}"/key.pem | tr '\n' '#') >> _env
echo TRITON_KEY_PATH=${TRITON_CREDS_PATH}/key.pem >> _env
echo TRITON_CERT=$(cat "${DOCKER_CERT_PATH}"/cert.pem | tr '\n' '#') >> _env
echo TRITON_CERT_PATH=${TRITON_CREDS_PATH}/cert.pem >> _env
echo SDC_KEY=$(cat "${TRITON_PRIVATE_KEY_PATH}" | tr '\n' '#') >> _env
echo SDC_KEY_PUB=$(cat "${TRITON_PRIVATE_KEY_PATH}".pub | tr '\n' '#') >> _env
echo NGINX_CA_CRT=$(cat "${NGINX_CA_CRT_PATH}" | tr '\n' '#') >> _env
echo NGINX_SERVER_KEY=$(cat "${NGINX_SERVER_KEY_PATH}" | tr '\n' '#') >> _env
echo NGINX_SERVER_CRT=$(cat "${NGINX_SERVER_CRT_PATH}" | tr '\n' '#') >> _env
echo >> _env
}
# ---------------------------------------------------
# parse arguments
# Get function list
funcs=($(declare -F -p | cut -d " " -f 3))
until
if [ ! -z "$1" ]; then
# check if the first arg is a function in this file, or use a default
if [[ " ${funcs[@]} " =~ " $1 " ]]; then
cmd=$1
shift 1
else
cmd="check"
fi
$cmd "$@"
if [ $? == 127 ]; then
help
fi
exit
else
help
fi
do
echo
done

64
bundle/src/images.js Normal file
View File

@ -0,0 +1,64 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-images');
const {
PORT = 4003,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'images',
DC_NAME,
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
const apiBaseUrl = SDC_URL;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

64
bundle/src/instances.js Normal file
View File

@ -0,0 +1,64 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-instances');
const {
PORT = 4002,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'instances',
DC_NAME,
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
const apiBaseUrl = SDC_URL;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

72
bundle/src/navigation.js Normal file
View File

@ -0,0 +1,72 @@
require('../.env.js');
const Main = require('apr-main');
const Nav = require('hapi-webconsole-nav');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-navigation');
const Regions = require('../data/regions');
const Categories = require('../data/categories');
const Account = require('../data/account');
const {
PORT = 4001,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'navigation',
DC_NAME,
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
const apiBaseUrl = SDC_URL;
const baseUrl = BASE_URL;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Nav,
options: {
keyPath,
keyId,
apiBaseUrl,
dcName,
baseUrl,
regions: Regions,
accountServices: Account,
categories: Categories
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

84
bundle/src/server.js Normal file
View File

@ -0,0 +1,84 @@
require('../.env.js');
const Hapi = require('hapi');
const Sso = require('hapi-triton-auth');
const {
COOKIE_PASSWORD,
COOKIE_DOMAIN,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID,
SDC_URL
} = process.env;
module.exports = async ({ PORT, BASE_URL }) => {
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
const apiBaseUrl = SDC_URL;
const ssoUrl = 'https://sso.joyent.com/login';
const baseUrl = BASE_URL;
const isDev = true;
const permissions = {
cloudapi: ['/my/*']
};
const cookie = {
password: COOKIE_PASSWORD,
domain: COOKIE_DOMAIN,
isSecure: false,
isHttpOnly: true,
ttl: 1000 * 60 * 60 // 1 hour
};
const server = Hapi.server({
port: PORT,
routes: {
cors: {
origin: ['*'],
credentials: true,
additionalHeaders: ['Cookie', 'X-CSRF-Token']
}
},
debug: {
log: ['error'],
request: ['error']
}
});
server.events.on('log', (event, tags) => {
if (tags.error) {
// eslint-disable-next-line no-console
console.log(event);
}
});
server.events.on('request', (request, event) => {
const { tags } = event;
if (tags.includes('error') && event.data && event.data.errors) {
event.data.errors.forEach(error => {
// eslint-disable-next-line no-console
console.log(error);
});
}
});
await server.register({
plugin: Sso,
options: {
keyPath,
keyId,
apiBaseUrl,
ssoUrl,
permissions,
baseUrl,
isDev,
cookie
}
});
server.auth.default('sso');
return server;
};

View File

@ -0,0 +1,78 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Tsg = require('tsg-graphql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-service-groups');
const {
PORT = 4004,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'service-groups',
DC_NAME,
TSG_URL = 'http://0.0.0.0:3000',
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Tsg,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: TSG_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: SDC_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

78
bundle/src/templates.js Normal file
View File

@ -0,0 +1,78 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Tsg = require('tsg-graphql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-templates');
const {
PORT = 4005,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'templates',
DC_NAME,
TSG_URL = 'http://0.0.0.0:3000',
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Tsg,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: TSG_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: SDC_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

View File

@ -4,7 +4,16 @@ module.exports = {
'scope-enum': [
2,
'always',
['ui-toolkit', 'icons', 'my-joy-beta', 'navigation', 'bundle']
[
'ui-toolkit',
'icons',
'instances',
'navigation',
'bundle',
'images',
'sg',
'templates'
]
]
}
};

View File

@ -0,0 +1,12 @@
{
"ignore": ["_document.js"],
"presets": [
[
"joyent-portal",
{
"aliases": true,
"autoAliases": true
}
]
]
}

View File

@ -21,3 +21,5 @@ yarn-error.log*
**/__diff_output__
lib/app

View File

@ -0,0 +1,27 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
## Image Snapshots Diff
**/__diff_output__
!lib/app
!dist
!build

View File

@ -0,0 +1,13 @@
{
"libs": ["ecmascript", "browser"],
"plugins": {
"doc_comment": true,
"local-scope": true,
"jsx": true,
"node": true,
"webpack": {
"configPath":
"../../node_modules/joyent-react-scripts/src/webpack.config.dev.js"
}
}
}

View File

@ -0,0 +1,133 @@
const Boom = require('boom');
const Inert = require('inert');
const Path = require('path');
const RenderReact = require('hapi-render-react');
const Intercept = require('apr-intercept');
const Fs = require('mz/fs');
const { NAMESPACE = 'images', NODE_ENV = 'development' } = process.env;
exports.register = async server => {
let manifest = {};
try {
manifest = require('../build/asset-manifest.json');
} catch (err) {
if (NODE_ENV === 'production') {
throw err;
} else {
// eslint-disable-next-line no-console
console.error(err);
}
}
const relativeTo = Path.join(__dirname, 'app');
const buildRoot = Path.join(__dirname, '../build');
const buildStatic = Path.join(buildRoot, `${NAMESPACE}`);
const publicRoot = Path.join(__dirname, `../public/static/`);
await server.register([
{
plugin: Inert
},
{
plugin: RenderReact
}
]);
server.route([
{
method: 'GET',
path: `/${NAMESPACE}/service-worker.js`,
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/service-worker.js')
}
}
}
},
{
method: 'GET',
path: `/${NAMESPACE}/favicon.ico`,
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/favicon.ico')
}
}
}
},
{
method: 'GET',
path: `/${NAMESPACE}/static/{rest*}`,
config: {
auth: false
},
handler: async (request, h) => {
const { params } = request;
const { rest } = params;
if (!rest) {
return Boom.notFound();
}
const publicPathname = Path.join(publicRoot, rest);
const [err1] = await Intercept(
Fs.access(publicPathname, Fs.constants.R_OK)
);
if (!err1) {
return h.file(publicPathname, {
confine: publicRoot
});
}
const buildPathname = Path.join(buildStatic, 'static', rest);
const [err2] = await Intercept(
Fs.access(buildPathname, Fs.constants.R_OK)
);
if (!err2) {
return h.file(buildPathname, {
confine: buildStatic
});
}
const filename = manifest[rest];
if (!filename) {
return Boom.notFound();
}
const buildMapPathname = Path.join(buildRoot, filename);
return h.file(buildMapPathname, {
confine: buildStatic
});
}
},
{
method: '*',
path: `/${NAMESPACE}/~server-error`,
handler: {
view: {
name: 'server-error',
relativeTo
}
}
},
{
method: '*',
path: `/${NAMESPACE}/{path*}`,
handler: {
view: {
name: 'app',
relativeTo
}
}
}
]);
};
exports.pkg = require('../package.json');

View File

@ -0,0 +1,82 @@
{
"name": "my-joy-images",
"version": "1.4.2",
"private": true,
"license": "MPL-2.0",
"repository": "github:yldio/joyent-portal",
"main": "lib/index.js",
"scripts": {
"dev": "REACT_APP_DEV=1 NAMESPACE=images NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3070 joyent-react-scripts start",
"build:test": "echo 0",
"build:lib": "echo 0",
"build:bundle": "NAMESPACE=images NODE_ENV=production redrun -p build:frontend build:ssr",
"prepublish": "NODE_ENV=production redrun build:bundle",
"test": "echo 0",
"test:ci": "echo 0",
"build:frontend": "joyent-react-scripts build",
"build:ssr": "SSR=1 UMD=1 babel src --out-dir lib/app --copy-files"
},
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2",
"apollo-cache-inmemory": "^1.2.2",
"apollo-client": "^2.3.2",
"apollo-link-http": "^1.5.4",
"apr-intercept": "^3.0.3",
"apr-reduce": "^3.0.3",
"boom": "^7.2.0",
"cross-fetch": "^2.2.0",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"exenv": "^1.2.2",
"force-array": "^3.1.0",
"fuse.js": "^3.2.0",
"hapi-render-react": "^2.5.2",
"hapi-render-react-joyent-document": "^7.2.0",
"inert": "^5.1.0",
"joyent-logo-assets": "^1.1.0",
"joyent-react-styled-flexboxgrid": "^3.1.0",
"joyent-ui-resource-widgets": "^1.0.0",
"joyent-ui-toolkit": "^6.0.0",
"lodash.assign": "^4.2.0",
"lodash.find": "^4.6.0",
"lodash.get": "^4.4.2",
"lodash.isfinite": "^3.3.2",
"lodash.isfunction": "^3.0.9",
"lodash.keys": "^4.2.0",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"lunr": "^2.2.1",
"mz": "^2.7.0",
"param-case": "^2.1.1",
"react": "^16.4.0",
"react-apollo": "^2.1.4",
"react-dom": "^16.4.0",
"react-helmet-async": "0.1.0",
"react-redux": "^5.0.7",
"react-redux-values": "^1.1.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"remcalc": "^1.0.10",
"styled-components": "^3.3.0",
"styled-components-spacing": "^3.0.0",
"styled-flex-component": "^2.2.2",
"styled-is": "^1.1.3",
"title-case": "^2.1.1",
"yup": "^0.25.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.19.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.4.2",
"jest-styled-components": "^5.0.1",
"joyent-react-scripts": "^8.2.1",
"react-screenshot-renderer": "^1.1.2",
"react-test-renderer": "^16.4.0",
"redrun": "^6.0.4"
}
}

View File

@ -0,0 +1,31 @@
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 400;
src: local('Libre Franklin'), local('LibreFranklin-Regular'),
url(../fonts/libre-franklin/libre-franklin-regular.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 500;
src: local('Libre Franklin Medium'), local('LibreFranklin-Medium'),
url(../fonts/libre-franklin/libre-franklin-medium.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 600;
src: local('Libre Franklin SemiBold'), local('LibreFranklin-SemiBold'),
url(../fonts/libre-franklin/libre-franklin-semibold.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 700;
src: local('Libre Franklin Bold'), local('LibreFranklin-Bold'),
url(../fonts/libre-franklin/libre-franklin-bold.ttf) format('truetype');
}

View File

@ -0,0 +1,15 @@
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: local('Roboto Mono'), local('RobotoMono-Regular'),
url(../fonts/roboto-mono/roboto-mono-regular.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'),
url(../fonts/roboto-mono/roboto-mono-bold.ttf) format('truetype');
}

View File

@ -0,0 +1,93 @@
Copyright (c) 2015, Impallari Type (www.impallari.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,7 +1,7 @@
# my-joy-beta
# my-joy-images
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg)](https://github.com/RichardLitt/standard-readme)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg?style=flat-square)](https://opensource.org/licenses/MPL-2.0)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
## Table of Contents

View File

@ -0,0 +1,10 @@
const { SSR } = process.env;
const aliases = {};
if (SSR) {
aliases['scroll-to-element'] = './src/mocks/scroll-to-element';
aliases['^joyent-ui-toolkit/dist/es/editor$'] = './src/mocks/editor';
}
module.exports = aliases;

View File

@ -0,0 +1,57 @@
const get = require('lodash.get');
const Document = require('hapi-render-react-joyent-document');
const path = require('path');
const url = require('url');
const { theme } = require('joyent-ui-toolkit');
const { default: createClient } = require('./state/apollo-client');
const { default: createStore } = require('./state/redux-store');
const indexFile = path.join(__dirname, '../../build/index.html');
const assets = require('../../build/asset-manifest.json');
const { NODE_ENV = 'development' } = process.env;
const getState = request => {
const { req } = request.raw;
const { headers } = req;
const { host } = headers;
const protocol = NODE_ENV === 'development' ? 'http:' : 'https:';
const _font = get(theme, 'font.href', () => '');
const _mono = get(theme, 'monoFont.href', () => '');
const _addr = url.parse(`${protocol}//${host}`);
const _theme = Object.assign({}, theme, {
font: Object.assign({}, theme.font, {
href: () =>
_font(
Object.assign(_addr, {
namespace: 'images'
})
)
}),
monoFont: Object.assign({}, theme.monoFont, {
href: () =>
_mono(
Object.assign(_addr, {
namespace: 'images'
})
)
})
});
return {
theme: _theme,
createClient,
createStore
};
};
module.exports = Document({
namespace: 'images/',
assets,
Html: require('./html'),
indexFile,
getState
});

View File

@ -0,0 +1,14 @@
import React from 'react';
import Helmet from 'react-helmet-async';
import { RootContainer } from 'joyent-ui-toolkit';
import Routes from '@root/routes';
export default () => (
<RootContainer>
<Helmet>
<title>Images</title>
</Helmet>
<Routes />
</RootContainer>
);

View File

@ -0,0 +1,71 @@
import React from 'react';
import { Field } from 'redux-form';
import { Margin } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import {
FormGroup,
FormLabel,
Input,
FormMeta,
Button,
RandomizeIcon,
Textarea
} from 'joyent-ui-toolkit';
export default ({ placeholderName, randomizing, onRandomize }) => (
<form>
<Flex wrap>
<FlexItem flex>
<FormGroup name="name" fluid field={Field}>
<FormLabel>Image name</FormLabel>
<Margin top="0.5">
<Input placeholder={placeholderName} onBlur={null} required />
</Margin>
<FormMeta />
</FormGroup>
</FlexItem>
<FlexItem>
<Margin left="1">
<Button
type="button"
onClick={onRandomize}
loading={randomizing}
marginless
secondary
icon
>
<RandomizeIcon />
<span>Randomize</span>
</Button>
</Margin>
</FlexItem>
</Flex>
<Margin top="3">
<FormGroup name="version" fluid field={Field}>
<FormLabel>Version</FormLabel>
<Margin top="0.5">
<Input placeholder="Example: v1.0" onBlur={null} required />
</Margin>
<FormMeta />
</FormGroup>
</Margin>
<Row>
<Col xs="12" sm="8">
<Margin top="3">
<FormGroup name="description" fluid field={Field}>
<FormLabel>Description</FormLabel>
<Margin top="0.5">
<Textarea
placeholder="Example: JarJarBinks, Anakin Skywalker, Obi Wan Kenobi, Qui-Gon Jinn, Han Solo, Wookies"
fluid
/>
</Margin>
<FormMeta />
</FormGroup>
</Margin>
</Col>
</Row>
</form>
);

View File

@ -16,14 +16,14 @@ const Container = styled.div`
export default ({ icon, children, collapsed = true, ...rest }) => (
<Container {...rest}>
<Flex>
<Margin right={1}>
<Margin right="1">
<Flex alignCenter full>
{icon}
</Flex>
</Margin>
<Small noMargin>{children}</Small>
</Flex>
<Margin top={1} bottom={collapsed ? 7 : 3}>
<Margin top="1" bottom={collapsed ? 7 : 3}>
<Divider height={remcalc(1)} />
</Margin>
</Container>

View File

@ -5,8 +5,8 @@ import { P } from 'joyent-ui-toolkit';
export default ({ children }) => (
<Row>
<Col xs={12} sm={8}>
<Margin bottom={3}>
<Col xs="12" sm="8">
<Margin bottom="3">
<P>{children}</P>
</Margin>
</Col>

View File

@ -4,10 +4,11 @@ import { Margin, Padding } from 'styled-components-spacing';
import Flex from 'styled-flex-component';
import { H3, Card } from 'joyent-ui-toolkit';
import NoPackagesImage from '@assets/no-packages.svg';
import { EmptyState } from 'joyent-icons';
const NoPackagesTitle = styled(H3)`
color: ${props => props.theme.greyDark};
text-align: center;
`;
const FullWidthCard = styled(Card)`
@ -16,10 +17,10 @@ const FullWidthCard = styled(Card)`
export default ({ children }) => (
<FullWidthCard>
<Padding all={6}>
<Padding all="6">
<Flex alignCenter justifyCenter column>
<Margin bottom={2}>
<img src={NoPackagesImage} alt="Sad Animal" />
<Margin bottom="2">
<EmptyState />
</Margin>
<NoPackagesTitle>{children}</NoPackagesTitle>
</Flex>

View File

@ -0,0 +1,209 @@
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Field } from 'redux-form';
import Flex, { FlexItem } from 'styled-flex-component';
import { Padding, Margin } from 'styled-components-spacing';
import remcalc from 'remcalc';
import {
Card,
Anchor,
CardHeader,
Divider,
ActionsIcon,
PopoverTarget,
Popover,
PopoverItem,
PopoverDivider as BasePopoverDivider,
PopoverContainer,
Radio,
FormLabel,
FormGroup,
StatusLoader
} from 'joyent-ui-toolkit';
import GLOBAL from '@state/global';
import { ImageType, OS } from '@root/constants';
const A = styled(Anchor)`
color: ${props => props.theme.text};
text-decoration: none;
font-weight: ${props => props.theme.font.weight.semibold};
`;
const CardAnchor = styled(Anchor)`
color: ${props => props.theme.text};
text-decoration: none;
`;
const ItemAnchor = styled(Anchor)`
color: ${props => props.theme.text};
-webkit-text-fill-color: currentcolor;
text-decoration: none;
`;
const Type = styled(Margin)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const Content = styled(Padding)`
max-width: calc(100% - ${remcalc(48)});
overflow: hidden;
`;
const Max = styled(Flex)`
max-width: 100%;
`;
const Actions = styled(Flex)`
width: ${remcalc(48)};
height: ${remcalc(48)};
min-width: ${remcalc(48)};
`;
const ActionsWrapper = styled(Flex)`
height: 100%;
width: 100%;
border-left: ${remcalc(1)} solid ${props => props.theme.grey};
`;
const PopoverDivider = styled(BasePopoverDivider)`
width: 100%;
`;
export const Image = ({
name,
id,
os,
version,
type,
removing,
onRemove,
onCreateInstance
}) => (
<Margin bottom="3">
<CardAnchor to={`/images/${id}`} component={Link}>
<Card radius>
{removing ? (
<Padding all="2">
<StatusLoader />
</Padding>
) : (
<Fragment>
<CardHeader white radius>
<Padding left="2" right="2">
<Flex full alignCenter>
<FlexItem>
<Margin right="2">
{React.createElement(OS[os], {
width: '24',
height: '24'
})}
</Margin>
</FlexItem>
<FlexItem>
<A to={`/images/${id}/summary`} component={Link}>
{name}
</A>
</FlexItem>
</Flex>
</Padding>
</CardHeader>
<Flex justifyBetween>
<Content left="2" top="2" bottom="2">
<Max justifyBetween>
<Max alignCenter>
<Flex>{version}</Flex>
<Divider vertical />
<Type>{ImageType[type]}</Type>
</Max>
</Max>
</Content>
<PopoverContainer clickable>
<Actions>
<PopoverTarget box>
<ActionsWrapper alignCenter justifyCenter>
<ActionsIcon />
</ActionsWrapper>
</PopoverTarget>
<Popover noPadding placement="bottom">
<Padding horizontal="3" vertical="2">
<PopoverItem disabled={false} onClick={onCreateInstance}>
<ItemAnchor
href={`${
GLOBAL.origin
}/instances/~create/?image=${name}`}
target="__blank"
rel="noopener noreferrer"
>
Create Instance
</ItemAnchor>
</PopoverItem>
</Padding>
<PopoverDivider />
<Padding horizontal="3" vertical="2">
<PopoverItem disabled={removing} onClick={onRemove}>
Remove
</PopoverItem>
</Padding>
</Popover>
</Actions>
</PopoverContainer>
</Flex>
</Fragment>
)}
</Card>
</CardAnchor>
</Margin>
);
export const Filters = ({ selected }) => (
<Fragment>
<FormGroup name="image-type" value="all" field={Field} type="radio">
<Radio>
<Flex alignCenter>
<Margin horizontal="2">
<FormLabel big normal={selected !== 'all'}>
All
</FormLabel>
</Margin>
</Flex>
</Radio>
</FormGroup>
<FormGroup
name="image-type"
value="hardware-virtual-machine"
field={Field}
type="radio"
>
<Radio noMargin>
<Flex alignCenter>
<Margin horizontal="2">
<FormLabel big normal={selected !== 'hardware-virtual-machine'}>
Virtual machines
</FormLabel>
</Margin>
</Flex>
</Radio>
</FormGroup>
<FormGroup
name="image-type"
value="infrastructure-container"
field={Field}
type="radio"
>
<Radio noMargin>
<Flex alignCenter>
<Margin horizontal="2">
<FormLabel big normal={selected !== 'infrastructure-container'}>
Infrastructure container
</FormLabel>
</Margin>
</Flex>
</Radio>
</FormGroup>
</Fragment>
);

View File

@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import forceArray from 'force-array';
import { Margin } from 'styled-components-spacing';
import { NavLink } from 'react-router-dom';
import forceArray from 'force-array';
import {
SectionList,
@ -29,20 +28,11 @@ const Menu = ({ links = [] }) => {
return (
<ViewContainer plain>
<Margin bottom={6}>
<Margin bottom="5" top="1">
<SectionList>{getMenuItems(_links)}</SectionList>
</Margin>
</ViewContainer>
);
};
Menu.propTypes = {
links: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
pathname: PropTypes.string
})
)
};
export default Menu;

View File

@ -0,0 +1,190 @@
import React, { Fragment } from 'react';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin, Padding } from 'styled-components-spacing';
import styled from 'styled-components';
import Flex, { FlexItem } from 'styled-flex-component';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import titleCase from 'title-case';
import remcalc from 'remcalc';
import { ValueBreakpoints as breakpoints } from 'joyent-ui-toolkit';
import {
Card,
CardOutlet,
H2,
P,
Label as BaseLabel,
Divider,
Button,
QueryBreakpoints,
CopiableField,
DuplicateIcon,
DeleteIcon,
DotIcon,
FormLabel,
Input
} from 'joyent-ui-toolkit';
import GLOBAL from '@state/global';
import { ImageType, OS } from '@root/constants';
const { SmallOnly, Medium } = QueryBreakpoints;
const VerticalDivider = styled.div`
width: ${remcalc(1)};
background: ${props => props.theme.grey};
height: ${remcalc(24)};
display: flex;
align-self: flex-end;
margin: 0 ${remcalc(18)};
@media (max-width: ${remcalc(breakpoints.small.upper)}) {
display: none;
}
`;
const Label = styled(BaseLabel)`
font-weight: 200;
`;
const GreyLabel = styled(Label)`
opacity: 0.5;
padding-right: ${remcalc(3)};
`;
const StateColor = {
ACTIVE: 'green',
UNACTIVATED: 'grey',
DISABLED: 'secondaryActive',
CREATING: 'primary',
FAILED: 'red'
};
// eslint-disable-next-line camelcase
export const Meta = ({ name, version, type, published_at, state, os }) => (
<Fragment>
<Flex alignCenter>
<FlexItem>
<Margin right="2">
{React.createElement(OS[os], {
width: '30',
height: '30'
})}
</Margin>
</FlexItem>
<FlexItem>
<H2 bold>{name}</H2>
</FlexItem>
</Flex>
<Margin top="2" bottom="3">
<Flex>
<Label>{version}</Label>
<VerticalDivider />
<Label>{ImageType[type]}</Label>
<VerticalDivider />
<Fragment>
<GreyLabel>Created:</GreyLabel>
<Label> {distanceInWordsToNow(published_at)} ago</Label>
</Fragment>
<VerticalDivider />
<Flex>
<FlexItem>
<DotIcon
right={remcalc(6)}
size={remcalc(15)}
color={StateColor[state]}
/>
</FlexItem>
<FlexItem>
<Label>{titleCase(state)}</Label>
</FlexItem>
</Flex>
</Flex>
</Margin>
</Fragment>
);
export default ({ theme = {}, onRemove, removing, ...image }) => (
<Row>
<Col xs="12" sm="12" md="9">
<Card>
<CardOutlet>
<Padding all="5">
<Meta {...image} />
<Row between="xs">
<Col xs="9">
<SmallOnly>
<Button type="button" small icon>
<DuplicateIcon light />
</Button>
</SmallOnly>
<Medium>
<Button
type="button"
href={`${GLOBAL.origin}/instances/~create/?image=${
image.id
}`}
target="__blank"
rel="noopener noreferrer"
bold
icon
>
<span>Create Instance</span>
</Button>
</Medium>
</Col>
<Col xs="3">
<SmallOnly>
<Button type="button" small icon error right>
<DeleteIcon fill="red" />
</Button>
</SmallOnly>
<Medium>
<Button
type="button"
loading={removing}
onClick={onRemove}
bold
icon
error
right
>
<Margin right="1">
<DeleteIcon fill="red" />
</Margin>
<span>Delete</span>
</Button>
</Medium>
</Col>
</Row>
<Margin bottom="4" top="4">
<Divider height={remcalc(1)} />
</Margin>
<Margin bottom="2">
<P>{image.description}</P>
</Margin>
<Margin bottom="3">
<CopiableField text={(image.id || '').split('-')[0]} label="ID" />
</Margin>
<Margin bottom="3">
<CopiableField text={image.id} label="UUID" />
</Margin>
<Row>
<Col xs="12" md="7">
<Margin bottom="3">
<FormLabel>Operating system</FormLabel>
<Input
monospace
onBlur={null}
fluid
value={titleCase(image.os)}
/>
</Margin>
</Col>
</Row>
</Padding>
</CardOutlet>
</Card>
</Col>
</Row>
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Margin } from 'styled-components-spacing';
import { TagItem } from 'joyent-ui-toolkit';
import KeyValue from '@components/key-value';
import { KeyValue } from 'joyent-ui-resource-widgets';
export const AddForm = props => (
<KeyValue {...props} method="add" input="input" type="tag" expanded />
@ -14,8 +14,8 @@ export const EditForm = props => (
export default ({ norMargin, name, value, onClick, onRemoveClick, active }) => (
<Margin
right={norMargin ? 0 : 1}
bottom={norMargin ? 0 : 1}
right={norMargin ? '0' : '1'}
bottom={norMargin ? '0' : '1'}
key={`${name}-${value}`}
>
<TagItem onClick={onClick} active={active} onRemoveClick={onRemoveClick}>

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Field } from 'redux-form';
import Flex from 'styled-flex-component';
import { Margin } from 'styled-components-spacing';
import { Button, FormGroup, Input, FormLabel } from 'joyent-ui-toolkit';
export const Toolbar = ({
searchable = true,
searchLabel = 'Filter',
searchPlaceholder = '',
action = false,
actionLabel = '',
actionable = false,
onActionClick
}) => (
<Flex justifyBetween alignEnd>
<FormGroup name="filter" field={Field}>
<FormLabel>{searchLabel}</FormLabel>
<Margin top="0.5">
<Input placeholder={searchPlaceholder} disabled={!searchable} />
</Margin>
</FormGroup>
{action ? (
<FormGroup right>
<Button
type="button"
disabled={!actionable}
onClick={onActionClick}
icon
fluid
>
{actionLabel}
</Button>
</FormGroup>
) : null}
</Flex>
);
export default ({ handleSubmit, ...rest }) => (
<form onSubmit={handleSubmit}>
<Toolbar {...rest} />
</form>
);

View File

@ -0,0 +1,37 @@
import {
Linux,
Freebsd,
Illumos,
Smart,
Windows,
Placeholder
} from 'joyent-logo-assets';
export const ImageType = {
ZONE_DATASET: 'Hardware Virtual Machine',
LX_DATASET: 'Infrastructure Container',
ZVOL: 'Hardware Virtual Machine',
DOCKER: 'Docker Container',
OTHER: 'Hardware Virtual Machine'
};
export const OS = {
SMARTOS: Smart,
LINUX: Linux,
WINDOWS: Windows,
BSD: Freebsd,
ILLUMOS: Illumos,
OTHER: Placeholder
};
export const Forms = {
FORM_TAGS_CREATE: 'CREATE-IMAGE-TAGS-ADD',
FORM_TAGS_EDIT: i => `CREATE-IMAGE-TAGS-EDIT-${i}`,
FORM_DETAILS: 'CREATE-IMAGE-DETAILS',
CREATE_FORM: 'CREATE-IMAGE',
CREATE_TAGS: 'CREATE-IMAGE-TAGS',
LIST_TOGGLE_TYPE_FORM: 'LIST-TOGGLE-TYPE-FORM',
LIST_TOOLBAR_FORM: 'LIST-TOOLBAR-FORM',
TAGS_TOOLBAR_FORM: 'TAGS-TOOLBAR-FORM',
TAGS_ADD_FORM: 'TAGS-ADD-FORM'
};

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Margin } from 'styled-components-spacing';
import paramCase from 'param-case';
import get from 'lodash.get';
import { Breadcrumb, BreadcrumbItem } from 'joyent-ui-toolkit';
export default ({ match }) => {
const image = get(match, 'params.image');
const create = get(match, 'params.step');
const instance = get(match, 'params.instance');
const links = [
{
name: 'Compute',
pathname: '/'
},
{
name: 'Images',
pathname: '/images'
}
]
.concat(
create && [
{
name: 'Create Image',
pathname: `/images/~create`
},
{
name: instance,
pathname: `/images/~create/${instance}`
}
]
)
.concat(
image && [
{
name: paramCase(image),
pathname: `/images/${image}`
}
]
)
.filter(Boolean)
.map(({ name, pathname }) => (
<BreadcrumbItem key={name} to={pathname} component={Link}>
<Margin horizontal="1" vertical="3">
{name}
</Margin>
</BreadcrumbItem>
));
return <Breadcrumb>{links}</Breadcrumb>;
};

View File

@ -0,0 +1,181 @@
import React, { Fragment } from 'react';
import { compose, graphql } from 'react-apollo';
import { set } from 'react-redux-values';
import ReduxForm from 'declarative-redux-form';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin } from 'styled-components-spacing';
import { change } from 'redux-form';
import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import { NameIcon, H3, Button, H4, P } from 'joyent-ui-toolkit';
import Title from '@components/create-image/title';
import Details from '@components/create-image/details';
import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import createClient from '@state/apollo-client';
import { instanceName as validateName } from '@state/validators';
import { Forms } from '@root/constants';
const NameContainer = ({
expanded,
proceeded,
name,
version,
description,
placeholderName,
randomizing,
handleAsyncValidate,
shouldAsyncValidate,
handleNext,
handleRandomize,
handleEdit,
step
}) => (
<Fragment>
<Title
id={step}
onClick={!expanded && !name && handleEdit}
collapsed={!expanded && !proceeded}
icon={<NameIcon />}
>
Image name and details
</Title>
{expanded ? (
<Description>
Here you can name your custom image, version it, and give it a
description so that you can identify it elsewhere in the Triton
ecosystem.
</Description>
) : null}
<ReduxForm
form={Forms.FORM_DETAILS}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleNext}
>
{props =>
expanded ? (
<Details
{...props}
placeholderName={placeholderName}
randomizing={randomizing}
onRandomize={handleRandomize}
/>
) : name ? (
<Margin top="3">
<H3 bold noMargin>
{name}
</H3>
{version ? (
<Margin top="2">
<H4 bold noMargin>
{version}
</H4>
</Margin>
) : null}
{description ? (
<Row>
<Col xs="12" sm="8">
<Margin top="1">
<P>{description}</P>
</Margin>
</Col>
</Row>
) : null}
</Margin>
) : null
}
</ReduxForm>
{expanded ? (
<Margin top="4" bottom="7">
<Button type="button" disabled={!name} onClick={handleNext}>
Next
</Button>
</Margin>
) : proceeded ? (
<Margin top="4" bottom="7">
<Button type="button" onClick={handleEdit} secondary>
Edit
</Button>
</Margin>
) : null}
</Fragment>
);
export default compose(
graphql(GetRandomName, {
options: () => ({
fetchPolicy: 'network-only',
ssr: false
}),
props: ({ data }) => ({
placeholderName: data.rndName || ''
})
}),
connect(
({ form, values }, ownProps) => {
const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
const description = get(
form,
`${Forms.FORM_DETAILS}.values.description`,
''
);
const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false);
const randomizing = get(values, 'create-image-name-randomizing', false);
return {
...ownProps,
proceeded,
randomizing,
name,
version,
description
};
},
(dispatch, { history, match }) => ({
handleNext: () => {
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/images/~create/${match.params.instance}/tag`);
},
handleEdit: () => {
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/images/~create/${match.params.instance}/name`);
},
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'change';
},
handleAsyncValidate: validateName,
handleRandomize: async () => {
dispatch(set({ name: 'create-image-name-randomizing', value: true }));
const [err, res] = await intercept(
createClient().query({
fetchPolicy: 'network-only',
query: GetRandomName
})
);
dispatch(set({ name: 'create-image-name-randomizing', value: false }));
if (err) {
// eslint-disable-next-line no-console
console.error(err);
return;
}
const { data } = res;
const { rndName } = data;
return dispatch(change(Forms.FORM_DETAILS, 'name', rndName));
}
})
)
)(NameContainer);

View File

@ -6,18 +6,16 @@ import { destroy, reset } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import get from 'lodash.get';
import remcalc from 'remcalc';
import Flex from 'styled-flex-component';
import { TagsIcon, Button, H3, TagList, Divider } from 'joyent-ui-toolkit';
import { TagsIcon, Button, H3, TagList } from 'joyent-ui-toolkit';
import { KeyValue } from 'joyent-ui-resource-widgets';
import Title from '@components/create-instance/title';
import Animated from '@containers/create-instance/animated';
import KeyValue from '@components/key-value';
import Title from '@components/create-image/title';
import Description from '@components/description';
import Tag from '@components/tags';
const FORM_NAME_CREATE = 'CREATE-INSTANCE-TAGS-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-TAGS-EDIT-${i}`;
import { addTag as validateTag } from '@state/validators';
import { Forms } from '@root/constants';
export const Tags = ({
tags = [],
@ -30,9 +28,12 @@ export const Tags = ({
handleToggleExpanded,
handleCancelEdit,
handleChangeAddOpen,
handleAsyncValidate,
shouldAsyncValidate,
handleNext,
step,
handleEdit
handleEdit,
children
}) => (
<Fragment>
<Title
@ -45,7 +46,7 @@ export const Tags = ({
</Title>
{expanded ? (
<Description>
Tags can be used to identify your instances, group multiple instances
Tags can be used to identify your images, group multiple images
together, define firewall and affinity rules, and more.{' '}
<a
target="__blank"
@ -58,7 +59,7 @@ export const Tags = ({
) : null}
{proceeded || expanded ? (
<Fragment>
<Margin bottom={4}>
<Margin bottom="5">
<H3>
{tags.length} Tag{tags.length === 1 ? '' : 's'}
</H3>
@ -76,9 +77,11 @@ export const Tags = ({
</Fragment>
) : null}
<ReduxForm
form={FORM_NAME_CREATE}
form={Forms.FORM_TAGS_CREATE}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleAddTag}
>
{props =>
@ -92,26 +95,26 @@ export const Tags = ({
expanded
onCancel={() => handleChangeAddOpen(false)}
/>
<Divider height={remcalc(18)} transparent />
</Fragment>
) : null
}
</ReduxForm>
{expanded ? (
<Margin top={1} bottom={7}>
<Button
type="button"
onClick={() => handleChangeAddOpen(true)}
secondary
>
Add Tag
</Button>
<Button type="button" onClick={handleNext}>
Next
</Button>
</Margin>
) : proceeded ? (
<Margin top={1} bottom={7}>
<Margin top="1">
<Flex alignCenter>
{expanded ? (
<Button
type="button"
onClick={() => handleChangeAddOpen(true)}
secondary
>
Add Tag
</Button>
) : null}
<Margin left="1">{children}</Margin>
</Flex>
</Margin>
{proceeded ? (
<Margin top="1">
<Button type="button" onClick={handleEdit} secondary>
Edit
</Button>
@ -121,33 +124,40 @@ export const Tags = ({
);
export default compose(
Animated,
connect(({ values }, ownProps) => ({
proceeded: get(values, 'create-instance-tags-proceeded', false),
addOpen: get(values, 'create-instance-tags-add-open', false),
tags: get(values, 'create-instance-tags', [])
proceeded: get(values, `${Forms.CREATE_TAGS}-proceeded`, false),
addOpen: get(values, `${Forms.CREATE_TAGS}-add-open`, false),
tags: get(values, Forms.CREATE_TAGS, [])
})),
connect(null, (dispatch, { tags = [], history }) => ({
connect(null, (dispatch, { tags = [], history, match }) => ({
handleNext: () => {
dispatch(set({ name: 'create-instance-tags-proceeded', value: true }));
dispatch(set({ name: `${Forms.CREATE_TAGS}-proceeded`, value: true }));
return history.push(`/instances/~create/metadata`);
return history.push(`/images/~create/${match.params.instance}/create`);
},
handleEdit: () => {
return history.push(`/instances/~create/tags`);
return history.push(`/images/~create/${match.params.instance}/tag`);
},
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleAddTag: value => {
const toggleToClosed = set({
name: `create-instance-tags-add-open`,
name: `${Forms.CREATE_TAGS}-add-open`,
value: false
});
const appendTag = set({
name: `create-instance-tags`,
name: Forms.CREATE_TAGS,
value: tags.concat([{ ...value, expanded: false }])
});
return dispatch([destroy(FORM_NAME_CREATE), toggleToClosed, appendTag]);
return dispatch([
destroy(Forms.FORM_TAGS_CREATE),
toggleToClosed,
appendTag
]);
},
handleUpdateTag: (index, newTag) => {
tags[index] = {
@ -156,14 +166,14 @@ export default compose(
};
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-instance-tags`, value: tags.slice() })
destroy(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
},
handleChangeAddOpen: value => {
return dispatch([
reset(FORM_NAME_CREATE),
set({ name: `create-instance-tags-add-open`, value })
reset(Forms.FORM_TAGS_CREATE),
set({ name: `${Forms.CREATE_TAGS}-add-open`, value })
]);
},
handleToggleExpanded: index => {
@ -174,7 +184,7 @@ export default compose(
return dispatch(
set({
name: `create-instance-tags`,
name: Forms.CREATE_TAGS,
value: tags.slice()
})
);
@ -186,16 +196,16 @@ export default compose(
};
return dispatch([
reset(FORM_NAME_EDIT(index)),
set({ name: `create-instance-tags`, value: tags.slice() })
reset(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
},
handleRemoveTag: index => {
tags.splice(index, 1);
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-instance-tags`, value: tags.slice() })
destroy(Forms.FORM_TAGS_EDIT(index)),
set({ name: Forms.CREATE_TAGS, value: tags.slice() })
]);
}
}))

View File

@ -0,0 +1,189 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import ReduxForm from 'declarative-redux-form';
import { destroyAll } from 'react-redux-values';
import { destroy, stopSubmit } from 'redux-form';
import { connect } from 'react-redux';
import { compose, graphql } from 'react-apollo';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import uniqBy from 'lodash.uniqby';
import omit from 'lodash.omit';
import {
ViewContainer,
H2,
Button,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import CreateImage from '@graphql/create-image.gql';
import GetInstance from '@graphql/get-instance.gql';
import Details from '@containers/create-image/details';
import Tags from '@containers/create-image/tags';
import { Forms } from '@root/constants';
import parseError from '@state/parse-error';
const Create = ({
step,
history,
location,
match,
disabled,
loading,
loadingError,
handleSubmit
}) => (
<ViewContainer>
{loading ? (
<Margin top="4">
<StatusLoader />
</Margin>
) : null}
{loadingError ? (
<Margin top="4">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{loadingError}</MessageDescription>
</Message>
</Margin>
) : null}
{!loading && !loadingError ? (
<Margin top="4" bottom="5">
<H2>Create Image</H2>
</Margin>
) : null}
{!loading && !loadingError ? (
<Details
history={history}
match={match}
step="name"
expanded={step === 'name'}
/>
) : null}
{!loading && !loadingError ? (
<Tags
history={history}
match={match}
step="tag"
expanded={step === 'tag'}
/>
) : null}
<ReduxForm form={Forms.CREATE_FORM} onSubmit={handleSubmit}>
{({ handleSubmit, submitting }) =>
!loading && !loadingError ? (
<form onSubmit={handleSubmit}>
<Margin top={step === 'tag' ? '7' : '4'}>
<Button disabled={disabled} loading={submitting}>
Create Image
</Button>
</Margin>
</form>
) : null
}
</ReduxForm>
</ViewContainer>
);
export default compose(
graphql(CreateImage, { name: 'createImage' }),
graphql(GetInstance, {
options: ({ match }) => ({
ssr: false,
variables: {
id: get(match, 'params.instance')
}
}),
props: ({ data: { loading, error, machine, variables, ...rest } }) => {
const notFoundMsg = `Instance "${variables.name}" not found!`;
const notFound = !loading && !machine ? notFoundMsg : false;
return {
instance: machine,
loadingError: error ? parseError(error) : notFound,
loading
};
}
}),
connect(({ form, values }, { match }) => {
const step = get(match, 'params.step', 'name');
const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
const disabled = !(name.length && version.length);
if (disabled) {
return { disabled, step };
}
const description = get(
form,
`${Forms.FORM_DETAILS}.values.description`,
'<instance-description>'
);
const tags = get(values, Forms.CREATE_TAGS, []);
return {
forms: Object.keys(form), // improve this
name,
description,
version,
tags,
disabled,
step
};
}),
connect(null, (dispatch, ownProps) => {
const {
name,
description,
version,
tags,
instance,
forms,
createImage,
history
} = ownProps;
return {
handleSubmit: async () => {
const _name = name.toLowerCase();
const _description = description.toLowerCase();
const _version = version.toLowerCase();
const _tags = uniqBy(tags, 'name').map(a => omit(a, 'expanded'));
const [err, res] = await intercept(
createImage({
variables: {
machine: instance.id,
name: _name,
version: _version,
description: _description,
tags: _tags
}
})
);
if (err) {
return dispatch(
stopSubmit(Forms.CREATE_FORM, {
_error: parseError(err)
})
);
}
dispatch([destroyAll(), forms.map(name => destroy(name))]);
const { data } = res;
const { createImageFromMachine } = data;
history.push(`/images/${createImageFromMachine.id}`);
}
};
})
)(Create);

View File

@ -0,0 +1,197 @@
import React, { Fragment } from 'react';
import { compose, graphql } from 'react-apollo';
import ReduxForm from 'declarative-redux-form';
import { Margin } from 'styled-components-spacing';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { connect } from 'react-redux';
import get from 'lodash.get';
import intercept from 'apr-intercept';
import { set } from 'react-redux-values';
import Fuse from 'fuse.js';
import {
ViewContainer,
Divider,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import GLOBAL from '@state/global';
import ToolbarForm from '@components/toolbar';
import Empty from '@components/empty';
import { ImageType, Forms } from '@root/constants';
import ListImages from '@graphql/list-images.gql';
import { Image, Filters } from '@components/image';
import RemoveImage from '@graphql/remove-image.gql';
import parseError from '@state/parse-error';
const { LIST_TOOLBAR_FORM, LIST_TOGGLE_TYPE_FORM } = Forms;
export const List = ({
images = [],
allImages = [],
loading = false,
error = null,
history,
typeValue,
handleCreateInstance,
handleRemove
}) => (
<ViewContainer main>
<Margin top="4">
<ReduxForm form={LIST_TOOLBAR_FORM}>
{props => <ToolbarForm {...props} actionable={!loading} />}
</ReduxForm>
</Margin>
<Margin vertical="4">
<Divider />
</Margin>
{loading && !images.length ? (
<Fragment>
<StatusLoader />
</Fragment>
) : null}
{error && !images.length && !loading ? (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your images
</MessageDescription>
</Message>
</Margin>
) : null}
<Fragment>
<Margin bottom="4">
<ReduxForm
form={LIST_TOGGLE_TYPE_FORM}
initialValues={{ 'image-type': 'all' }}
>
{props =>
allImages.length ? (
<Filters selected={typeValue} {...props} />
) : null
}
</ReduxForm>
</Margin>
<Row>
{images.map(image => (
<Col sm="4">
<Image
{...image}
onCreateInstance={() => handleCreateInstance(image)}
onRemove={() => handleRemove(image.id)}
/>
</Col>
))}
{!images.length && !loading ? (
<Empty>No images to see here</Empty>
) : null}
</Row>
</Fragment>
</ViewContainer>
);
export default compose(
graphql(RemoveImage, {
name: 'removeImage'
}),
graphql(ListImages, {
options: () => ({
ssr: false,
pollInterval: 1000
}),
props: ({ data: { images = [], loading, error, refetch } }) => ({
images: images || [],
index: new Fuse(images || [], {
keys: ['name', 'os', 'version', 'state', 'type']
}),
loading,
error
})
}),
connect(
({ form, values }, { index, error, images = [] }) => {
const filter = get(form, `${LIST_TOOLBAR_FORM}.values.filter`, false);
const mutationError = get(values, 'remove-mutation-error', null);
const typeValue = get(
form,
`${LIST_TOGGLE_TYPE_FORM}.values.image-type`,
'all'
);
const virtual = Object.keys(ImageType).filter(
i => ImageType[i] === 'Hardware Virtual Machine'
);
const container = Object.keys(ImageType).filter(
i => ImageType[i] === 'Infrastructure Container'
);
const filtered = filter ? index.search(filter) : images;
return {
images: filtered
.filter(image => {
switch (typeValue) {
case 'all':
return true;
case 'hardware-virtual-machine':
return virtual.includes(image.type);
case 'infrastructure-container':
return container.includes(image.type);
default:
return true;
}
})
.map(({ id, ...image }) => ({
...image,
id,
removing: get(values, `remove-mutation-${id}-loading`, false)
})),
allImages: images,
mutationError,
typeValue
};
},
(dispatch, { removeImage, history }) => ({
handleCreateInstance: image => {
return window
.open(
`${GLOBAL.origin}/instances/~create/?image=${image.name}`,
'_blank'
)
.focus();
},
handleRemove: async id => {
dispatch([set({ name: `remove-mutation-${id}-loading`, value: true })]);
const [err, res] = await intercept(
removeImage({
variables: {
id
}
})
);
if (err) {
dispatch([
set({ name: 'remove-mutation-error', value: parseError(err) }),
set({ name: `remove-mutation-${id}-loading`, value: false })
]);
}
if (res) {
dispatch(
set({ name: `remove-mutation-${id}-loading`, value: false })
);
history.push('/images');
}
}
})
)
)(List);

View File

@ -0,0 +1,21 @@
import React from 'react';
import get from 'lodash.get';
import Menu from '@components/menu';
const SECTIONS = [
{ name: 'Summary', pathname: 'summary' },
{ name: 'Tags', pathname: 'tags' }
];
export default ({ match }) => {
const imageId = get(match, 'params.image');
const sections = imageId === '~create' ? [] : SECTIONS;
const links = sections.map(({ name, pathname }) => ({
name,
pathname: `/images/${imageId}/${pathname}`
}));
return <Menu links={links} />;
};

View File

@ -0,0 +1,113 @@
import React from 'react';
import { compose, graphql } from 'react-apollo';
import { Margin } from 'styled-components-spacing';
import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import { set } from 'react-redux-values';
import get from 'lodash.get';
import {
ViewContainer,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import ImageSummary from '@components/summary';
import GetImage from '@graphql/get-image.gql';
import RemoveImage from '@graphql/remove-image.gql';
import parseError from '@state/parse-error';
export const Summary = ({
image,
loading = false,
error = null,
removing,
mutationError,
handleRemove
}) => (
<ViewContainer main>
{loading && !image ? <StatusLoader /> : null}
{error && !loading && !image ? (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance summary
</MessageDescription>
</Message>
</Margin>
) : null}
{mutationError ? (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
There was a problem deleting your image
</MessageDescription>
</Message>
</Margin>
) : null}
{image ? (
<ImageSummary removing={removing} onRemove={handleRemove} {...image} />
) : null}
</ViewContainer>
);
export default compose(
graphql(RemoveImage, { name: 'removeImage' }),
graphql(GetImage, {
options: ({ match }) => ({
ssr: false,
variables: {
id: get(match, 'params.image')
}
}),
props: ({ data }) => {
const { loading = false, error = null, image } = data;
return {
image,
loading,
error
};
}
}),
connect(
({ values }, ownProps) => {
const removing = get(values, 'remove-mutation-loading', false);
const mutationError = get(values, 'remove-mutation-error', null);
return {
...ownProps,
removing,
mutationError
};
},
(dispatch, { removeImage, image, history }) => ({
handleRemove: async () => {
dispatch(set({ name: 'remove-mutation-loading', value: true }));
const [err, res] = await intercept(
removeImage({
variables: {
id: image.id
}
})
);
if (err) {
dispatch([
set({ name: 'remove-mutation-error', value: parseError(err) }),
set({ name: 'remove-mutation-loading', value: false })
]);
}
if (res) {
dispatch(set({ name: 'remove-mutation-loading', value: false }));
history.push('/images');
}
}
})
)
)(Summary);

View File

@ -0,0 +1,231 @@
import React from 'react';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { Margin } from 'styled-components-spacing';
import ReduxForm from 'declarative-redux-form';
import { destroy } from 'redux-form';
import { set } from 'react-redux-values';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import remcalc from 'remcalc';
import Fuse from 'fuse.js';
import {
H3,
ViewContainer,
StatusLoader,
Message,
MessageTitle,
MessageDescription,
TagList,
Divider
} from 'joyent-ui-toolkit';
import { Forms } from '@root/constants';
import Tag, { AddForm } from '@components/tags';
import ToolbarForm from '@components/toolbar';
import UpdateImageTags from '@graphql/update-image-tags.gql';
import GetTags from '@graphql/get-tags.gql';
import { addTag as validateTag } from '@state/validators';
import parseError from '@state/parse-error';
const { TAGS_TOOLBAR_FORM, TAGS_ADD_FORM } = Forms;
export const Tags = ({
tags = [],
addOpen = false,
loading = false,
error = null,
mutationError = null,
mutating = false,
handleAsyncValidate,
shouldAsyncValidate,
handleToggleAddOpen,
handleRemoveTag,
handleAddTag
}) => (
<ViewContainer main>
<ReduxForm form={TAGS_TOOLBAR_FORM}>
{props => (
<Margin bottom="4">
<ToolbarForm
{...props}
searchable={!loading}
actionLabel="Add Tag"
actionable={!loading && !mutating && !addOpen}
onActionClick={handleToggleAddOpen}
action
/>
<Margin vertical="4">
<Divider height={remcalc(1)} />
</Margin>
</Margin>
)}
</ReduxForm>
{error && !loading && !tags.length ? (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance tags
</MessageDescription>
</Message>
</Margin>
) : null}
{mutationError ? (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{mutationError}</MessageDescription>
</Message>
</Margin>
) : null}
<ReduxForm
form={TAGS_ADD_FORM}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleAddTag}
>
{props =>
addOpen ? (
<Margin bottom="5">
<AddForm
{...props}
onToggleExpanded={() => handleToggleAddOpen(!addOpen)}
onCancel={() => handleToggleAddOpen(!addOpen)}
/>
</Margin>
) : null
}
</ReduxForm>
{loading ? null : (
<Margin bottom="5">
<H3>
{tags.length} tag{tags.length === 1 ? '' : 's'}
</H3>
</Margin>
)}
{loading && !tags.length ? <StatusLoader /> : null}
<TagList>
{tags.map(({ id, name, value }) => (
<Tag
key={id}
id={id}
name={name}
value={value}
onRemoveClick={!mutating && (() => handleRemoveTag(name))}
active
/>
))}
</TagList>
</ViewContainer>
);
export default compose(
graphql(UpdateImageTags, {
name: 'updateTags'
}),
graphql(GetTags, {
options: ({ match }) => ({
ssr: false,
fetchPolicy: 'network-only',
pollInterval: 1000,
variables: {
id: get(match, 'params.image')
}
}),
props: ({ data }) => {
const { loading = false, error = null, image, refetch } = data;
const tags = get(image || {}, 'tags', []);
const index = new Fuse(tags, {
keys: ['name', 'value']
});
return {
index,
image: image || {},
tags,
loading,
error,
refetch
};
}
}),
connect(
({ values, form }, { index, tags, image }) => {
const filter = get(form, `${TAGS_TOOLBAR_FORM}.values.filter`, false);
const filtered = filter ? index.search(filter) : tags;
return {
tags: filtered,
addOpen: get(values, `${image.id}-add-open`, false),
mutationError: get(values, `${image.id}-mutation-error`, false),
mutating: get(values, `${image.id}-mutating`, false)
};
},
(dispatch, { image, tags = [], updateTags, refetch }) => ({
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleToggleAddOpen: addOpen => {
dispatch(set({ name: `${image.id}-add-open`, value: addOpen }));
},
handleRemoveTag: async name => {
dispatch(set({ name: `${image.id}-mutating`, value: true }));
const [err] = await intercept(
updateTags({
variables: {
id: image.id,
tags: tags
.map(({ name, value }) => ({ name, value }))
.filter(tag => tag.name !== name)
}
})
);
if (err) {
dispatch([
set({ name: `${image.id}-mutation-error`, value: parseError(err) }),
set({ name: `${image.id}-mutating`, value: false })
]);
}
await refetch();
dispatch(set({ name: `${image.id}-mutating`, value: false }));
},
handleAddTag: async ({ name, value }) => {
dispatch(set({ name: `${image.id}-mutating`, value: true }));
const [err] = await intercept(
updateTags({
variables: {
id: image.id,
tags: tags
.map(({ name, value }) => ({ name, value }))
.concat([{ name, value }])
}
})
);
if (err) {
dispatch([
set({ name: `${image.id}-mutation-error`, value: parseError(err) }),
set({ name: `${image.id}-mutating`, value: false })
]);
}
await refetch();
dispatch([
set({ name: `${image.id}-mutating`, value: false }),
dispatch(set({ name: `${image.id}-add-open`, value: false })),
destroy(TAGS_ADD_FORM)
]);
}
})
)
)(Tags);

View File

@ -0,0 +1,18 @@
mutation createImage(
$machine: ID!
$name: String!
$version: String!
$description: String
$tags: [KeyValueInput]
) {
createImageFromMachine(
machine: $machine
name: $name
version: $version
description: $description
tags: $tags
) {
id
name
}
}

View File

@ -0,0 +1,19 @@
query image($id: ID) {
image(id: $id) {
id
name
os
version
description
type
homepage
published_at
owner
public
state
error {
code
message
}
}
}

View File

@ -0,0 +1,6 @@
query instance($id: ID) {
machine(id: $id) {
id
name
}
}

View File

@ -0,0 +1,3 @@
query rndImageName {
rndName
}

View File

@ -0,0 +1,11 @@
query image($id: ID) {
image(id: $id) {
id
name
tags {
id
name
value
}
}
}

View File

@ -0,0 +1,18 @@
query images {
images(public: false) {
id
name
os
version
type
homepage
published_at
owner
public
state
error {
code
message
}
}
}

View File

@ -0,0 +1,6 @@
mutation removeImage($id: ID!) {
deleteImage(id: $id) {
id
name
}
}

View File

@ -0,0 +1,5 @@
mutation updateImageTags($id: ID!, $tags: [KeyValueInput]!) {
updateImage(id: $id, tags: $tags) {
id
}
}

View File

@ -0,0 +1,28 @@
const React = require('react');
module.exports = ({
htmlAttrs = {},
bodyAttrs = {},
head = [],
children = null
}) => (
<html {...htmlAttrs}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="theme-color" content="#1E313B" />
{head}
</head>
<body {...bodyAttrs}>
<div id="header" />
{children ? null : <div id="root" />}
{children}
<script src="/navigation/static/main.js" />
</body>
</html>
);

View File

@ -0,0 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HelmetProvider } from 'react-helmet-async';
import { ThemeProvider } from 'styled-components';
import { Provider as ReduxProvider } from 'react-redux';
import { ApolloProvider } from 'react-apollo';
import { BrowserRouter } from 'react-router-dom';
import isFunction from 'lodash.isfunction';
import isFinite from 'lodash.isfinite';
import theme from '@state/theme';
import createStore from '@state/redux-store';
import createClient from '@state/apollo-client';
import App from './app';
if (!isFunction(Number.isFinite)) {
Number.isFinite = isFinite;
}
ReactDOM.hydrate(
<ApolloProvider client={createClient()}>
<ThemeProvider theme={theme}>
<ReduxProvider store={createStore()}>
<BrowserRouter>
<HelmetProvider context={{}}>
<App />
</HelmetProvider>
</BrowserRouter>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>,
document.getElementById('root')
);

View File

@ -0,0 +1,8 @@
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import createClient from '@state/apollo-client';
export default ({ children }) => (
<ApolloProvider client={createClient()}>{children}</ApolloProvider>
);

View File

@ -0,0 +1,122 @@
import React, { Fragment } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import get from 'lodash.get';
import {
PageContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle,
Footer
} from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/breadcrumb';
import Menu from '@containers/menu';
import List from '@containers/list';
import Summary from '@containers/summary';
import Create from '@containers/create';
import Tags from '@containers/tags';
import { Route as ServerError } from '@root/server-error';
const { REACT_APP_DEV = false } = process.env;
export default () => (
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/images/~server-error" component={Breadcrumb} />
<Route
path="/images/~create/:instance/:step?"
exact
component={Breadcrumb}
/>
<Route path="/images/:image?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/images/~server-error" component={() => null} />
<Route path="/images/:image/:section?" component={Menu} />
<Route path="/images/~create/:instance/:step?" component={() => {}} />
</Switch>
{/* Images */}
<Switch>
{/* <Route path="/images/~server-error" component={() => null} /> */}
<Route path="/images/" exact component={List} />
<Route path="/images/:image/summary" exact component={Summary} />
<Route path="/images/:image/tags" exact component={Tags} />
<Route
path="/images/:image"
exact
component={({ match }) => (
<Redirect to={`/images/${get(match, 'params.image')}/summary`} />
)}
/>
</Switch>
{/* Create Image */}
<Switch>
<Route
path="/images/~create/:instance?"
exact
component={({ match }) => (
<Redirect to={`/images/~create/${match.params.instance}/name`} />
)}
/>
<Route path="/images/~create/:instance/:step" component={Create} />
</Switch>
<Route path="/images/~server-error" component={ServerError} />
<Route path="/" exact component={() => <Redirect to="/images" />} />
{REACT_APP_DEV ? (
<Fragment>
<Route
path="/instances"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3069${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/templates"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3071${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/service-groups"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3072${
location.pathname
}${location.search}`
)
}
/>
</Fragment>
) : null}
<noscript>
<ViewContainer main>
<Message warning>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
You need to enable JavaScript to run this app.
</MessageDescription>
</Message>
</ViewContainer>
</noscript>
<Footer />
</PageContainer>
);

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import remcalc from 'remcalc';
import {
RootContainer,
PageContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle,
Divider
} from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/breadcrumb';
export const Route = () => (
<ViewContainer main>
<Divider height={remcalc(30)} transparent />
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your page
</MessageDescription>
</Message>
</Margin>
</ViewContainer>
);
export default () => (
<RootContainer>
<PageContainer>
<Breadcrumb />
<Route />
</PageContainer>
</RootContainer>
);

View File

@ -0,0 +1,42 @@
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'cross-fetch';
import get from 'lodash.get';
import global from './global';
const {
REACT_APP_GQL_PORT = global.port,
REACT_APP_GQL_PROTOCOL = global.protocol,
REACT_APP_GQL_HOSTNAME = global.hostname
} = process.env;
const PORT = REACT_APP_GQL_PORT ? `:${REACT_APP_GQL_PORT}` : '';
const URI = `${REACT_APP_GQL_PROTOCOL}://${REACT_APP_GQL_HOSTNAME}${PORT}/images/graphql`;
export default (opts = {}, request = {}) => {
const host = get(request, 'raw.req.headers.host', '');
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
cache = cache.restore(global.__APOLLO_STATE__);
}
return new ApolloClient({
cache,
link: new HttpLink({
uri: host ? `${REACT_APP_GQL_PROTOCOL}//${host}/images/graphql` : URI,
credentials: 'same-origin',
fetch,
headers: {
'X-CSRF-Token': global.cookie.replace(
/(?:(?:^|.*;\s*)crumb\s*=\s*([^;]*).*$)|^.*$/,
'$1'
)
}
}),
...opts
});
};

View File

@ -0,0 +1,29 @@
import { canUseDOM } from 'exenv';
import queryString from 'query-string';
const { NODE_ENV = 'development' } = process.env;
export const Global = () => {
if (!canUseDOM) {
return {
protocol: NODE_ENV === 'development' ? 'http:' : 'https:',
cookie: ''
};
}
return {
port: window.location.port,
protocol: window.location.protocol.replace(/:$/, ''),
hostname: window.location.hostname,
pathname: window.location.pathname,
origin: window.location.origin,
cookie: document.cookie || '',
search: window.location.search,
query: queryString.parse(window.location.search || ''),
__REDUX_DEVTOOLS_EXTENSION__: window.__REDUX_DEVTOOLS_EXTENSION__,
__APOLLO_STATE__: window.__APOLLO_STATE__,
__REDUX_STATE__: window.__REDUX_STATE__
};
};
export default Global();

View File

@ -0,0 +1,26 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { reducer as valuesReducer } from 'react-redux-values';
import global from './global';
const initialState = {};
export default () => {
return createStore(
combineReducers({
values: valuesReducer,
form: formReducer,
ui: (state = {}) => state
}),
global.__REDUX_STATE__ || initialState,
compose(
reduxBatch,
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition
typeof global.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined'
? global.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
)
);
};

View File

@ -0,0 +1,21 @@
import { theme } from 'joyent-ui-toolkit';
const font = theme.font.href({
namespace: 'images'
});
const monoFont = theme.monoFont.href({
namespace: 'images'
});
export default {
...theme,
font: {
...theme.font,
href: () => font
},
monoFont: {
...theme.monoFont,
href: () => monoFont
}
};

Some files were not shown because too many files have changed in this diff Show More