Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit d94ebc9

Browse files
committed
externalize application name and other theming
Fixes #353
1 parent 3c3d6af commit d94ebc9

File tree

20 files changed

+385
-47
lines changed

20 files changed

+385
-47
lines changed

app/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
# sometimes, `@openwhisk/composer` appears at the top level `app/plugins/node_modules/@openwhisk/composer`,
44
# and other times it appears in `app/plugins/node_modules/wskng-composer-plugins/node_modules/@openwhisk/composer`
55
package-lock.json
6-
plugins/package-lock.json
6+
plugins/package-lock.json
7+
8+
build

app/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Cloud Shell Developer Help
2+
3+
This document is intended to assist developers of the Shell.
4+
5+
## Theming and Customization
6+
7+
You may choose to customize the branding and look of the Shell. A
8+
custom theme starts with a settings file. An example is provided in
9+
`config/envs/ibm.json`. This file indicates:
10+
11+
- The name of your product, via the `productName` field. The header of the Shell breaks this down into a `companyName`, a `productSubName`, and a name for the tool itself, `shellName`. You will probably choose to define productName as the concatenation of these three sub-fields, but are free to choose otherwise.
12+
13+
- The application icon: `appIcon`
14+
15+
- A large format icon that will appear in the About window: `largeIcon`
16+
17+
- The css theme file to use, `cssTheme`. Define this as the name of a file in `content/css/themes`, but specify only the base name, not the full path to this CSS theme file.
18+
19+
- Optionally, you may set the User-Agent header that will be communicated to the OpenWhisk backend: `userAgent`

app/bin/build.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Copyright 2018 IBM Corporation
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
const debug = require('debug')('build'),
20+
color = require('colors'),
21+
path = require('path'),
22+
fs = require('fs'),
23+
{ parseOptions } = require(path.join(__dirname, '../config/options')),
24+
{ parseConfig } = require(path.join(__dirname, '../config/config'))
25+
26+
/**
27+
* Tell the user where we're at
28+
*
29+
*/
30+
const task = taskName => console.log('task: '.dim + taskName)
31+
32+
/**
33+
* Read in index.html
34+
*
35+
*/
36+
const readIndex = options => new Promise((resolve, reject) => {
37+
const { templateDir } = options
38+
39+
debug('read template', templateDir)
40+
task('read index.html template')
41+
42+
fs.readFile(path.join(templateDir, 'index.html'), (err, data) => {
43+
if (err) {
44+
reject(err)
45+
} else {
46+
resolve(data.toString())
47+
}
48+
})
49+
})
50+
51+
/**
52+
* Evaluate macros in the given string, using the given setting of configurations
53+
*
54+
*/
55+
const evaluateMacros = settings => str => {
56+
debug('settings', settings)
57+
task('evaluate macros')
58+
59+
for (let key in settings) {
60+
str = str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), settings[key])
61+
}
62+
63+
return str
64+
}
65+
66+
/**
67+
* Ensure that the buildDir exists
68+
*
69+
*/
70+
const makeBuildDir = options => new Promise((resolve, reject) => {
71+
const { buildDir } = options
72+
73+
fs.mkdir(buildDir, err => {
74+
if (err && err.code !== 'EEXIST') {
75+
reject(err)
76+
} else {
77+
resolve()
78+
}
79+
})
80+
})
81+
82+
/**
83+
* Write the updated index.html
84+
*
85+
*/
86+
const writeIndex = options => str => new Promise((resolve, reject) => {
87+
const { appName, buildDir } = options
88+
89+
task('write index.html')
90+
91+
fs.writeFile(path.join(buildDir, 'index.html'), str, err => {
92+
if (err) {
93+
reject(err)
94+
} else {
95+
resolve()
96+
}
97+
})
98+
})
99+
100+
/**
101+
* Stash the chosen configuration settings to the buildDir, and update
102+
* the app/package.json so that the productName field reflects the
103+
* chosen setting
104+
*
105+
*/
106+
const writeConfig = (options, settings) => new Promise((resolve, reject) => {
107+
const { buildDir } = options
108+
109+
task('write config')
110+
111+
fs.writeFile(path.join(buildDir, 'config.json'), JSON.stringify(settings, undefined, 4), err => {
112+
if (err) {
113+
reject(err)
114+
} else {
115+
task('write package.json')
116+
117+
const topLevel = require(path.join(__dirname, '../package.json')),
118+
packageJson = Object.assign({}, topLevel, settings)
119+
120+
fs.writeFile(path.join(buildDir, 'package.json'), JSON.stringify(packageJson, undefined, 4), err => {
121+
if (err) {
122+
reject(err)
123+
} else {
124+
resolve()
125+
}
126+
})
127+
}
128+
})
129+
})
130+
131+
/**
132+
* Do a build
133+
*
134+
*/
135+
const doBuild = (options, settings) => () => Promise.all([ writeConfig(options, settings),
136+
readIndex(options)
137+
.then(evaluateMacros(settings))
138+
.then(writeIndex(options))
139+
])
140+
141+
/**
142+
* Either project out a field of the configuration settings, or do a build
143+
*
144+
*/
145+
const doWork = (options, settings) => {
146+
if (options.get) {
147+
if (settings[options.get]) {
148+
// we were asked only to respond with a projection of the config settings
149+
debug('projecting', options.get, settings[options.get])
150+
return Promise.resolve(settings[options.get])
151+
}
152+
153+
return Promise.reject('error: '.red + 'requested field ' + options.get.blue + ' not found in config file')
154+
}
155+
156+
return makeBuildDir(options)
157+
.then(doBuild(options, settings))
158+
.then(() => 'ok:'.green + ' build successful')
159+
}
160+
161+
/**
162+
* Build index.html
163+
*
164+
*/
165+
const buildIndex = parseOptions() // read options
166+
.then(options => parseConfig(options) // read chosen configuration settings
167+
.then(settings => doWork(options, settings))) // either project out a setting, or do a build
168+
169+
.then(console.log)
170+
.catch(err => {
171+
console.error(err)
172+
process.exit(1)
173+
})

app/bin/seticon.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
# TODO this only handles MacOS right now
2020

2121
#ICON=../assets/icons/png/OpenWhisk-512x512.png
22-
ICON=../assets/icons/png/blue-dolphin-transparent.png
22+
ICON=`cat ./build/config.json | jq --raw-output .appIcon`
2323

24-
APPNAME="IBM Cloud Functions Shell"
24+
APPNAME=`cat ./build/config.json | jq --raw-output .productName`
2525

26-
if [ ! -x ./node_modules/.bin/fileicon ]; then exit; fi
26+
echo "Using appName=${APPNAME} and appIcon=${ICON}"
27+
28+
if [ ! -x ./node_modules/.bin/fileicon ]; then
29+
echo "Not setting icon, as fileicon npm not installed"
30+
exit;
31+
fi
2732

2833
if [ $? == 0 ]; then
2934
./node_modules/.bin/fileicon set ./node_modules/electron/dist/Electron.app/ $ICON >& /dev/null

app/config/config.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2018 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const debug = require('debug')('config parser'),
18+
path = require('path')
19+
20+
/**
21+
* Default options
22+
*
23+
*/
24+
const defaultOptions = () => {
25+
debug('using default options')
26+
27+
const { parseOptions } = require('./options')
28+
return parseOptions()
29+
}
30+
31+
/**
32+
* Parse the variable mapping file
33+
*
34+
*/
35+
exports.parseConfig = (options=defaultOptions()) => new Promise((resolve, reject) => {
36+
if (!options.config) {
37+
reject('Please specify a config file, either via a CONFIG env var or a --config command line option')
38+
} else {
39+
resolve(require(path.join(__dirname, './envs/', options.config)))
40+
}
41+
})

app/config/envs/ibm.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"productName": "IBM Cloud Shell",
3+
"companyName": "IBM",
4+
"productSubName": "Cloud",
5+
"shellName": "Shell",
6+
"userAgent": "IBM Cloud Functions Shell",
7+
"cssTheme": "ibm.css",
8+
"appIcon": "../assets/icons/png/blue-dolphin-transparent.png",
9+
"largeIcon": "content/icons/svg/blue-dolphin.svg"
10+
}

app/config/options.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2018 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const debug = require('debug')('options parser'),
18+
minimist = require('yargs-parser'),
19+
path = require('path')
20+
21+
/**
22+
* Remove undefined mappings from the given map
23+
*
24+
*/
25+
const nix = map => {
26+
const nixed = {}
27+
28+
for (let key in map) {
29+
if (map[key] !== undefined) {
30+
nixed[key] = map[key]
31+
}
32+
}
33+
34+
return nixed
35+
}
36+
37+
/**
38+
* Parse options
39+
*
40+
*/
41+
exports.parseOptions = () => new Promise((resolve, reject) => {
42+
debug('parsing options')
43+
44+
// options from environment variables
45+
const envOptions = nix({
46+
config: process.env.CONFIG
47+
})
48+
49+
// default value assignments for options
50+
const defaultOptions = {
51+
config: 'ibm.json',
52+
templateDir: path.join(__dirname, '../templates'),
53+
buildDir: path.join(__dirname, '../build')
54+
}
55+
56+
// options from command line flags
57+
const cliOptions = minimist(process.argv.slice(2))
58+
59+
// union the three
60+
const options = Object.assign(defaultOptions, envOptions, cliOptions)
61+
debug('options', options)
62+
63+
resolve(options)
64+
})

app/content/css/themes/ibm.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ body a:hover:not(.plain-anchor) {
5050
.header {
5151
background: var(--color-stripe-01);
5252
color: var(--color-ui-01);
53+
border-bottom-color: var(--color-ui-05);
5354
outline-color: var(--color-ui-05); /* outline, rather than border-bottom-color, to allow minimized sidecar to stay on top */
5455
box-shadow: 0 1px 2px rgba(0,0,0,0.1);;
5556
}

app/content/css/ui.css

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ body a:hover:not(.plain-anchor) {
4545
padding-right: 3em;
4646
font-weight: 300;
4747
font-size: 1rem;
48-
outline: 1px solid; /* outline rather than border-bottom, to allow minimized sidecar to stay on top */
48+
border-bottom: 1px solid; /* outline rather than border-bottom, to allow minimized sidecar to stay on top */
4949
-webkit-font-smoothing: antialiased; /* chrome, you suck */
5050
}
51+
body.sidecar-is-minimized .header {
52+
outline: 1px solid; /* outline rather than border-bottom, to allow minimized sidecar to stay on top */
53+
border-bottom: none;
54+
}
5155
.header .deemphasize {
5256
font-size: 55%;
5357
letter-spacing: 1px;
@@ -1289,11 +1293,18 @@ body.os-darwin:not(.fullscreen) .header {
12891293
body .header .application-icon {
12901294
position: absolute;
12911295
top: 0.4em;
1292-
right: 0; /*0.5em;*/
1296+
right: 0.5em;
12931297
width: 1.8em;
12941298
height: 1.8em;
12951299
margin: 0;
12961300
z-index: 20;
1301+
transition: all 150ms ease-in-out;
1302+
}
1303+
body.sidecar-is-minimized .header .application-icon {
1304+
/* scoot the icon to be flush right when the sidecar is minimized, so
1305+
that the icon will be horizontally centered on the vertical stripe
1306+
that represents the minimized sidecar */
1307+
right: 0;
12971308
}
12981309
body.subwindow.still-loading .header .application-icon {
12991310
transition: all 150ms ease-in-out;

0 commit comments

Comments
 (0)