feat: initial import
1
.browserslistrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Chrome 98
|
||||
18
.editorconfig
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# https://github.com/jokeyrhyme/standard-editorconfig
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# defaults
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
59
.electron-builder.config.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
if (process.env.VITE_APP_VERSION === undefined) {
|
||||
const now = new Date;
|
||||
process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
const config = {
|
||||
productName: "container-desktop",
|
||||
appId: "com.example.yourapp",
|
||||
directories: {
|
||||
output: 'dist',
|
||||
buildResources: 'buildResources',
|
||||
},
|
||||
files: [
|
||||
'packages/**/dist/**',
|
||||
'extensions/**/builtin/*.cdix/**',
|
||||
],
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 410,
|
||||
y: 150,
|
||||
type: "link",
|
||||
path: "/Applications",
|
||||
},
|
||||
{
|
||||
x: 130,
|
||||
y: 150,
|
||||
type: "file",
|
||||
},
|
||||
],
|
||||
},
|
||||
extraMetadata: {
|
||||
version: process.env.VITE_APP_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
4
.electron-vendors.cache.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"chrome": "98",
|
||||
"node": "16"
|
||||
}
|
||||
54
.eslintrc.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"browser": false
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
/** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"packages/preload/exposedInMainWorld.d.ts",
|
||||
"node_modules/**",
|
||||
"**/dist/**"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
|
||||
/**
|
||||
* Having a semicolon helps the optimizer interpret your code correctly.
|
||||
* This avoids rare errors in optimized code.
|
||||
* @see https://twitter.com/alex_kozack/status/1364210394328408066
|
||||
*/
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
/**
|
||||
* This will make the history of changes in the hit a little cleaner
|
||||
*/
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
"always-multiline"
|
||||
],
|
||||
/**
|
||||
* Just for beauty
|
||||
*/
|
||||
"quotes": [
|
||||
"warn", "single"
|
||||
]
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
.eslintcache
|
||||
*.cdix
|
||||
202
LICENSE
Normal 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.
|
||||
0
README.md
Normal file
0
buildResources/.gitkeep
Normal file
BIN
buildResources/icon.icns
Normal file
BIN
buildResources/icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
59
buildResources/icon.svg
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
viewBox="0 0 40 40"
|
||||
id="svg12"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/Users/benoitf/git/benoitf/container-desktop/buildResources/icon.png"
|
||||
inkscape:export-xdpi="1228.8"
|
||||
inkscape:export-ydpi="1228.8"
|
||||
inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs16" />
|
||||
<sodipodi:namedview
|
||||
id="namedview14"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.41875"
|
||||
inkscape:cx="19.941577"
|
||||
inkscape:cy="20.097371"
|
||||
inkscape:window-width="1696"
|
||||
inkscape:window-height="1140"
|
||||
inkscape:window-x="750"
|
||||
inkscape:window-y="272"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg12" />
|
||||
<g
|
||||
transform="matrix(1.1473 0 0 1.1473 1.9288 -4.2099)"
|
||||
id="g10">
|
||||
<path
|
||||
d="m15 16.5 15.502 7.0687-15.502 12.931-14.502-12.069z"
|
||||
fill="#c4c4c4"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m15.502 11.703 15.502 7.0687-15.502 12.931-14.502-12.069z"
|
||||
fill="#e9e9e9"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m15.502 11.703 15.502 7.0687-15.502 12.931-14.502-12.069z"
|
||||
fill="#e9e9e9"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m15.502 5.7033 15.502 7.0687-15.502 12.931-14.502-12.069z"
|
||||
fill="#7a59fa"
|
||||
id="path8" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
extensions/crc/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
33
extensions/crc/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "crc",
|
||||
"displayName": "crc extension",
|
||||
"description": "crc extension",
|
||||
"version": "0.0.1",
|
||||
"icon": "icon.png",
|
||||
"publisher": "benoitf",
|
||||
"license": "apache-2.0",
|
||||
"engines": {
|
||||
"container-desktop": "^0.0.1"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "docker.info",
|
||||
"title": "crc: Specific info about crc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./scripts/build.js",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmpwip/extension-api": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"7zip-min": "^1.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
}
|
||||
}
|
||||
46
extensions/crc/scripts/build.js
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env node
|
||||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
const zipper = require('zip-local');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
|
||||
const builtinDirectory = path.resolve(__dirname, '../builtin');
|
||||
const unzippedDirectory = path.resolve(builtinDirectory, `${package.name}.cdix`);
|
||||
// remove the .cdix file before zipping
|
||||
if (fs.existsSync(destFile)) {
|
||||
fs.rmSync(destFile);
|
||||
}
|
||||
// remove the builtin folder before zipping
|
||||
if (fs.existsSync(builtinDirectory)) {
|
||||
fs.rmSync(builtinDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
|
||||
|
||||
|
||||
|
||||
// create unzipped built-in
|
||||
mkdirp(unzippedDirectory).then(() => {
|
||||
zipper.sync.unzip(destFile).save(unzippedDirectory);
|
||||
});
|
||||
56
extensions/crc/src/extension.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@tmpwip/extension-api';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
|
||||
|
||||
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
|
||||
let socketPath;
|
||||
const isWindows = os.platform() === 'win32';
|
||||
if (isWindows) {
|
||||
socketPath = '//./pipe/crc-podman';
|
||||
} else {
|
||||
socketPath = path.resolve(os.homedir(), '.crc/machines/crc/docker.sock');
|
||||
}
|
||||
|
||||
|
||||
const dockerContainerProvider: extensionApi.ContainerProvider = {
|
||||
provideName: () => 'crc/podman',
|
||||
|
||||
provideConnection: async (): Promise<string> => {
|
||||
return socketPath;
|
||||
},
|
||||
};
|
||||
if (fs.existsSync(socketPath)) {
|
||||
const disposable = await extensionApi.container.registerContainerProvider(dockerContainerProvider);
|
||||
extensionContext.subscriptions.push(disposable);
|
||||
console.log('crc extension is active');
|
||||
} else {
|
||||
console.error(`Could not find crc podman socket at ${socketPath}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
console.log('stopping crc extension');
|
||||
}
|
||||
|
||||
18
extensions/crc/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"webworker"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
BIN
extensions/docker/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
33
extensions/docker/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "docker",
|
||||
"displayName": "docker extension",
|
||||
"description": "docker extension",
|
||||
"version": "0.0.1",
|
||||
"icon": "icon.png",
|
||||
"publisher": "benoitf",
|
||||
"license": "apache-2.0",
|
||||
"engines": {
|
||||
"container-desktop": "^0.0.1"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "docker.info",
|
||||
"title": "Docker: Specific info about Docker"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./scripts/build.js",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmpwip/extension-api": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"7zip-min": "^1.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
}
|
||||
}
|
||||
48
extensions/docker/scripts/build.js
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env node
|
||||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
const zipper = require('zip-local');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
|
||||
const builtinDirectory = path.resolve(__dirname, '../builtin');
|
||||
const unzippedDirectory = path.resolve(builtinDirectory, `${package.name}.cdix`);
|
||||
// remove the .cdix file before zipping
|
||||
if (fs.existsSync(destFile)) {
|
||||
fs.rmSync(destFile);
|
||||
}
|
||||
// remove the builtin folder before zipping
|
||||
if (fs.existsSync(builtinDirectory)) {
|
||||
fs.rmSync(builtinDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
|
||||
|
||||
|
||||
|
||||
// create unzipped built-in
|
||||
/*
|
||||
mkdirp(unzippedDirectory).then(() => {
|
||||
zipper.sync.unzip(destFile).save(unzippedDirectory);
|
||||
});
|
||||
*/
|
||||
46
extensions/docker/src/extension.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@tmpwip/extension-api';
|
||||
import * as os from 'os';
|
||||
|
||||
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
|
||||
let socketPath: string;
|
||||
const isWindows = os.platform() === 'win32';
|
||||
if (isWindows) {
|
||||
socketPath = '//./pipe/docker_engine';
|
||||
} else {
|
||||
socketPath = '/var/run/docker.sock';
|
||||
}
|
||||
const dockerContainerProvider: extensionApi.ContainerProvider = {
|
||||
provideName: () => 'Docker',
|
||||
|
||||
provideConnection: async (): Promise<string> => {
|
||||
return socketPath;
|
||||
},
|
||||
};
|
||||
|
||||
const disposable = await extensionApi.container.registerContainerProvider(dockerContainerProvider);
|
||||
extensionContext.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
console.log('stopping docker extension');
|
||||
}
|
||||
|
||||
18
extensions/docker/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"webworker"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
BIN
extensions/lima/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
33
extensions/lima/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "lima",
|
||||
"displayName": "lima extension",
|
||||
"description": "lima extension",
|
||||
"version": "0.0.1",
|
||||
"icon": "icon.png",
|
||||
"publisher": "benoitf",
|
||||
"license": "apache-2.0",
|
||||
"engines": {
|
||||
"container-desktop": "^0.0.1"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "docker.info",
|
||||
"title": "Lima: Specific info about Lima"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./scripts/build.js",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmpwip/extension-api": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"7zip-min": "^1.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
}
|
||||
}
|
||||
30
extensions/lima/scripts/build.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env node
|
||||
const zipper = require('zip-local');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
|
||||
const builtinDirectory = path.resolve(__dirname, '../builtin');
|
||||
const unzippedDirectory = path.resolve(builtinDirectory, `${package.name}.cdix`);
|
||||
// remove the .cdix file before zipping
|
||||
if (fs.existsSync(destFile)) {
|
||||
fs.rmSync(destFile);
|
||||
}
|
||||
// remove the builtin folder before zipping
|
||||
if (fs.existsSync(builtinDirectory)) {
|
||||
fs.rmSync(builtinDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
|
||||
|
||||
|
||||
|
||||
// create unzipped built-in
|
||||
/*
|
||||
mkdirp(unzippedDirectory).then(() => {
|
||||
zipper.sync.unzip(destFile).save(unzippedDirectory);
|
||||
});
|
||||
*/
|
||||
47
extensions/lima/src/extension.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@tmpwip/extension-api';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
|
||||
const socketPath = path.resolve(os.homedir(), '.lima/podman/sock/podman.sock');
|
||||
|
||||
const dockerContainerProvider: extensionApi.ContainerProvider = {
|
||||
provideName: () => 'Lima',
|
||||
|
||||
provideConnection: async (): Promise<string> => {
|
||||
return socketPath;
|
||||
},
|
||||
};
|
||||
if (fs.existsSync(socketPath)) {
|
||||
const disposable = await extensionApi.container.registerContainerProvider(dockerContainerProvider);
|
||||
extensionContext.subscriptions.push(disposable);
|
||||
console.log('Lima extension is active');
|
||||
} else {
|
||||
console.error(`Could not find podman socket at ${socketPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
console.log('stopping lima extension');
|
||||
}
|
||||
|
||||
18
extensions/lima/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"webworker"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
BIN
extensions/podman/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
33
extensions/podman/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "podman",
|
||||
"displayName": "podman extension",
|
||||
"description": "podman extension",
|
||||
"version": "0.0.1",
|
||||
"icon": "icon.png",
|
||||
"publisher": "benoitf",
|
||||
"license": "apache-2.0",
|
||||
"engines": {
|
||||
"container-desktop": "^0.0.1"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "docker.info",
|
||||
"title": "podman: Specific info about podman"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./scripts/build.js",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmpwip/extension-api": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"7zip-min": "^1.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
}
|
||||
}
|
||||
46
extensions/podman/scripts/build.js
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env node
|
||||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
const zipper = require('zip-local');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
|
||||
const builtinDirectory = path.resolve(__dirname, '../builtin');
|
||||
const unzippedDirectory = path.resolve(builtinDirectory, `${package.name}.cdix`);
|
||||
// remove the .cdix file before zipping
|
||||
if (fs.existsSync(destFile)) {
|
||||
fs.rmSync(destFile);
|
||||
}
|
||||
// remove the builtin folder before zipping
|
||||
if (fs.existsSync(builtinDirectory)) {
|
||||
fs.rmSync(builtinDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
|
||||
|
||||
|
||||
|
||||
// create unzipped built-in
|
||||
mkdirp(unzippedDirectory).then(() => {
|
||||
zipper.sync.unzip(destFile).save(unzippedDirectory);
|
||||
});
|
||||
202
extensions/podman/src/extension.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import * as extensionApi from '@tmpwip/extension-api';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import {spawn} from 'child_process';
|
||||
|
||||
const podmanMachineSocketsDirectory = path.resolve(os.homedir(), '.local/share/containers/podman/machine');
|
||||
const podmanMachineQemuDirectory = path.resolve(podmanMachineSocketsDirectory, 'qemu');
|
||||
const watchers = new Map<string, fs.FSWatcher>();
|
||||
const currentProviders: extensionApi.Disposable[] = [];
|
||||
const lifecycleProviders = new Map<string, extensionApi.ContainerProviderLifecycle>();
|
||||
let storedExtensionContext;
|
||||
let refreshMachine = false;
|
||||
let stopLoop = false;
|
||||
/*
|
||||
function unregisterProviders() {
|
||||
currentProviders.forEach(p => p.dispose());
|
||||
}
|
||||
*/
|
||||
async function initMachines() {
|
||||
|
||||
// we search for all sockets and try to connect if possible
|
||||
const children = await fs.promises.readdir(podmanMachineSocketsDirectory, { withFileTypes: true });
|
||||
// grab all directories except qemu one
|
||||
const directories = children.filter(c => c.isDirectory() && c.name !== 'qemu').map(c => c.name);
|
||||
|
||||
// ok now for each directory, register a provider if socket is working
|
||||
await Promise.all(directories.map(async (directory) => {
|
||||
// podman.sock link
|
||||
const socketPath = path.resolve(podmanMachineSocketsDirectory, directory, 'podman.sock');
|
||||
const available = await socketAvailable(socketPath);
|
||||
let status = 'stopped';
|
||||
if (available) {
|
||||
registerProviderFor(directory, socketPath);
|
||||
status = 'started';
|
||||
}
|
||||
registerProviderLifecycle(directory, status);
|
||||
|
||||
// monitor qemu file
|
||||
const children = await fs.promises.readdir(podmanMachineQemuDirectory, { withFileTypes: true });
|
||||
const qemuFile = children.filter(c => c.isFile() && c.name.startsWith(`${directory}_`)).map(c => c.name);
|
||||
if (qemuFile.length === 1) {
|
||||
monitorQemuMachine(path.resolve(podmanMachineQemuDirectory, qemuFile[0]));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
async function timeout(time: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshMachinesCron() {
|
||||
|
||||
// we need to refresh as during the last 5 seconds, we need a refresh
|
||||
if (refreshMachine) {
|
||||
await Promise.all(currentProviders.map(provider => provider.dispose()));
|
||||
currentProviders.length = 0;
|
||||
initMachines();
|
||||
// we disable the refresh for a while
|
||||
refreshMachine = false;
|
||||
}
|
||||
|
||||
// call us again
|
||||
if (!stopLoop) {
|
||||
await timeout(5000);
|
||||
refreshMachinesCron();
|
||||
}
|
||||
}
|
||||
|
||||
async function monitorQemuMachine(qemuFileToWatch) {
|
||||
if (watchers.get(qemuFileToWatch)) {
|
||||
// already watching, skip
|
||||
return;
|
||||
}
|
||||
const watcher = fs.watch(qemuFileToWatch, () => {
|
||||
// we refresh the list of machines every time there is a new event in that directory
|
||||
refreshMachine = true;
|
||||
});
|
||||
watchers.set(qemuFileToWatch, watcher);
|
||||
|
||||
}
|
||||
|
||||
async function registerProviderFor(directory: string, socketPath: string) {
|
||||
const provider: extensionApi.ContainerProvider = {
|
||||
provideName: () => `podman-machine-${directory}`,
|
||||
|
||||
provideConnection: async (): Promise<string> => {
|
||||
return socketPath;
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Registering podman provider for', directory);
|
||||
const disposable = await extensionApi.container.registerContainerProvider(provider);
|
||||
currentProviders.push(disposable);
|
||||
storedExtensionContext.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
function execPromise(command, args) {
|
||||
return new Promise((resolve) => {
|
||||
spawn(command, args);
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function registerProviderLifecycle(directory: string, status: string) {
|
||||
const providerLifecycle: extensionApi.ContainerProviderLifecycle = {
|
||||
provideName: () => `podman-machine-${directory}`,
|
||||
status: () => status,
|
||||
|
||||
start: async (): Promise<void> =>{
|
||||
// start the machine
|
||||
console.log(`executing podman machine start ${directory}`);
|
||||
await execPromise('podman', ['machine', 'start', directory]);
|
||||
// wait before machine is up
|
||||
await timeout(10000);
|
||||
console.log('machine is started !');
|
||||
},
|
||||
stop: async (): Promise<void> =>{
|
||||
console.log(`executing podman machine stop ${directory}`);
|
||||
await execPromise('podman', ['machine', 'stop', directory]);
|
||||
// wait before machine is stopped
|
||||
await timeout(5000);
|
||||
console.log('machine is stopped !');
|
||||
},
|
||||
handleLifecycleChange: async (callback: (event: string) => void): Promise<void> => {
|
||||
callback('started');
|
||||
},
|
||||
};
|
||||
console.log('Registering podman provider lifecyclefor', directory);
|
||||
const disposable = await extensionApi.container.registerContainerProviderLifecycle(providerLifecycle);
|
||||
// currentProviders.push(disposable);
|
||||
storedExtensionContext.subscriptions.push(disposable);
|
||||
lifecycleProviders.set(directory, providerLifecycle);
|
||||
|
||||
}
|
||||
|
||||
async function socketAvailable(socketPath): Promise<boolean> {
|
||||
|
||||
const client = new net.Socket();
|
||||
const promise = new Promise<boolean>((res) => {
|
||||
client.connect(
|
||||
socketPath,
|
||||
() => {
|
||||
// stop connection
|
||||
client.destroy();
|
||||
res(true);
|
||||
},
|
||||
);
|
||||
client.on('error', () => res(false));
|
||||
});
|
||||
return promise;
|
||||
|
||||
}
|
||||
|
||||
|
||||
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
|
||||
storedExtensionContext = extensionContext;
|
||||
|
||||
// no podman for now, skip
|
||||
if (!fs.existsSync(podmanMachineSocketsDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// track all podman machines available
|
||||
fs.watch(podmanMachineSocketsDirectory, () => {
|
||||
// we refresh the list of machines every time there is a new event in that directory
|
||||
refreshMachine = true;
|
||||
});
|
||||
|
||||
// do the first initialization
|
||||
initMachines();
|
||||
refreshMachinesCron();
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
stopLoop = true;
|
||||
console.log('stopping podman extension');
|
||||
}
|
||||
|
||||
18
extensions/podman/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2017",
|
||||
"webworker"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
73
package.json
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"name": "container-desktop",
|
||||
"license": "apache-2.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=v16.13",
|
||||
"npm": ">=8.1"
|
||||
},
|
||||
"main": "packages/main/dist/index.cjs",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"extensions/*"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:main && npm run build:preload && npm run build:renderer && npm run build:extensions",
|
||||
"build:main": "cd ./packages/main && vite build",
|
||||
"build:extensions": "npm run build:extensions:docker && npm run build:extensions:lima && npm run build:extensions:crc && npm run build:extensions:podman",
|
||||
"build:extensions:crc": "cd ./extensions/crc && npm run build",
|
||||
"build:extensions:docker": "cd ./extensions/docker && npm run build",
|
||||
"build:extensions:lima": "cd ./extensions/lima && npm run build",
|
||||
"build:extensions:podman": "cd ./extensions/podman && npm run build",
|
||||
"build:extension-api": "cd ./packages/extension-api && vite build",
|
||||
"build:preload": "cd ./packages/preload && vite build",
|
||||
"build:preload:types": "dts-cb -i \"packages/preload/tsconfig.json\" -o \"packages/preload/exposedInMainWorld.d.ts\"",
|
||||
"build:renderer": "vite -c packages/renderer/vite.config.js build",
|
||||
"compile": "cross-env MODE=production npm run build && electron-builder build -mwl --config .electron-builder.config.js --dir --config.asar=false",
|
||||
"compile:dmg": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js",
|
||||
"test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e",
|
||||
"test:e2e": "npm run build && vitest run",
|
||||
"test:main": "vitest run -r packages/main --passWithNoTests",
|
||||
"test:preload": "vitest run -r packages/preload --passWithNoTests",
|
||||
"test:renderer": "vitest run -r packages/renderer --passWithNoTests",
|
||||
"watch": "node scripts/watch.js",
|
||||
"lint": "eslint . --ext js,ts,svelte",
|
||||
"typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
|
||||
"typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
|
||||
"typecheck:renderer": "npm run build:preload:types",
|
||||
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@types/dockerode": "^3.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||
"@typescript-eslint/parser": "^5.11.0",
|
||||
"cross-env": "7.0.3",
|
||||
"dts-for-context-bridge": "0.7.1",
|
||||
"electron": "17.0.0",
|
||||
"electron-builder": "22.14.13",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"eslint": "8.8.0",
|
||||
"happy-dom": "2.31.1",
|
||||
"nano-staged": "0.5.0",
|
||||
"playwright": "1.18.1",
|
||||
"postcss-import": "^14.0.2",
|
||||
"simple-git-hooks": "2.7.0",
|
||||
"typescript": "4.5.5",
|
||||
"vite": "2.7.13",
|
||||
"vitest": "0.2.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerode": "^3.3.1",
|
||||
"electron-updater": "4.6.5",
|
||||
"tar-fs": "^2.1.1"
|
||||
},
|
||||
"resolutionsComments": {
|
||||
"ssh2": "Need to use an old version of ssh2 to avoid vite/rollup issue on loading some internal lib"
|
||||
},
|
||||
"resolutions": {
|
||||
"ssh2": "0.8.9"
|
||||
}
|
||||
}
|
||||
21
packages/extension-api/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@tmpwip/extension-api",
|
||||
"version": "0.0.1",
|
||||
"description": "Prototype API",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "apache-2.0",
|
||||
"types": "./src/extension-api.d.ts",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "",
|
||||
"clean": "rimraf lib *.tsbuildinfo",
|
||||
"build": "",
|
||||
"watch": "",
|
||||
"publish:next": "yarn publish --registry=https://registry.npmjs.org/ --no-git-tag-version --new-version 0.0.1-\"$(date +%s)\""
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
93
packages/extension-api/src/extension-api.d.ts
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
declare module '@tmpwip/extension-api' {
|
||||
|
||||
/**
|
||||
* Represents a reference to a command. Provides a title which
|
||||
* will be used to represent a command in the UI and, optionally,
|
||||
* an array of arguments which will be passed to the command handler
|
||||
* function when invoked.
|
||||
*/
|
||||
export interface Command {
|
||||
title: string;
|
||||
command: string;
|
||||
tooltip?: string;
|
||||
arguments?: any[];
|
||||
}
|
||||
|
||||
export class Disposable {
|
||||
|
||||
constructor(func: () => void);
|
||||
/**
|
||||
* Dispose this object.
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
static create(func: () => void): Disposable;
|
||||
|
||||
/**
|
||||
* Combine many disposable-likes into one. Use this method
|
||||
* when having objects with a dispose function which are not
|
||||
* instances of Disposable.
|
||||
*
|
||||
* @param disposableLikes Objects that have at least a `dispose`-function member.
|
||||
* @return Returns a new disposable which, upon dispose, will
|
||||
* dispose all provided disposables.
|
||||
*/
|
||||
static from(...disposableLikes: { dispose: () => any }[]): Disposable;
|
||||
|
||||
/**
|
||||
* Creates a new Disposable calling the provided function
|
||||
* on dispose.
|
||||
* @param callOnDispose Function that disposes something.
|
||||
*/
|
||||
constructor(callOnDispose: Function);
|
||||
|
||||
}
|
||||
|
||||
export interface ExtensionContext {
|
||||
readonly subscriptions: { dispose(): any }[];
|
||||
}
|
||||
|
||||
export interface ContainerProviderLifecycle {
|
||||
provideName(): string;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
status(): string;
|
||||
handleLifecycleChange(callback: (event: string) => void): Promise<void>
|
||||
|
||||
}
|
||||
|
||||
export interface ContainerProvider {
|
||||
provideName(): string;
|
||||
provideConnection(): PromiseLike<string>;
|
||||
}
|
||||
|
||||
export namespace commands {
|
||||
export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable;
|
||||
export function executeCommand<T = unknown>(command: string, ...rest: any[]): PromiseLike<T>;
|
||||
}
|
||||
|
||||
export namespace container {
|
||||
export function registerContainerProvider(provider: ContainerProvider): PromiseLike<Disposable>;
|
||||
export function registerContainerProviderLifecycle(providerLifecycle: ContainerProviderLifecycle): PromiseLike<Disposable>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
packages/extension-api/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
36
packages/extension-api/vite.config.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* eslint-env node */
|
||||
import {join} from 'path';
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
},
|
||||
},
|
||||
plugins: [svelte()],
|
||||
optimizeDeps: {
|
||||
exclude: ['tinro']
|
||||
},
|
||||
base: '',
|
||||
server: {
|
||||
fs: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
|
||||
emptyOutDir: true,
|
||||
brotliSize: false,
|
||||
},
|
||||
})
|
||||
101
packages/main/src/index.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import trayIcon from './tray-icon.png';
|
||||
import {app, Menu, nativeImage, Tray} from 'electron';
|
||||
import './security-restrictions';
|
||||
import {restoreOrCreateWindow} from '/@/mainWindow';
|
||||
|
||||
|
||||
/**
|
||||
* Prevent multiple instances
|
||||
*/
|
||||
const isSingleInstance = app.requestSingleInstanceLock();
|
||||
if (!isSingleInstance) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
app.on('second-instance', restoreOrCreateWindow);
|
||||
|
||||
|
||||
/**
|
||||
* Disable Hardware Acceleration for more power-save
|
||||
*/
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
/**
|
||||
* Shout down background process if all windows was closed
|
||||
*/
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
|
||||
*/
|
||||
app.on('activate', restoreOrCreateWindow);
|
||||
|
||||
|
||||
/**
|
||||
* Create app window when background process will be ready
|
||||
*/
|
||||
app.whenReady()
|
||||
.then(restoreOrCreateWindow)
|
||||
.catch((e) => console.error('Failed create window:', e));
|
||||
|
||||
|
||||
/**
|
||||
* Install some other devtools in development mode only
|
||||
*/
|
||||
/*
|
||||
if (import.meta.env.DEV) {
|
||||
app.whenReady()
|
||||
.then(() => import('electron-devtools-installer'))
|
||||
.then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
|
||||
loadExtensionOptions: {
|
||||
allowFileAccess: true,
|
||||
},
|
||||
}))
|
||||
.catch(e => console.error('Failed install extension:', e));
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check new app version in production mode only
|
||||
*/
|
||||
if (import.meta.env.PROD) {
|
||||
app.whenReady()
|
||||
.then(() => import('electron-updater'))
|
||||
.then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify())
|
||||
.catch((e) => console.error('Failed check updates:', e));
|
||||
}
|
||||
|
||||
const nativeTrayIcon = nativeImage.createFromDataURL(trayIcon);
|
||||
|
||||
let tray = null
|
||||
app.whenReady().then(() => {
|
||||
tray = new Tray(nativeTrayIcon)
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Podman', type: 'radio' },
|
||||
{ label: 'Kubernetes', type: 'separator' },
|
||||
{ label: 'OpenShift', type: 'radio', checked: true },
|
||||
])
|
||||
tray.setContextMenu(contextMenu)
|
||||
})
|
||||
99
packages/main/src/mainWindow.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain} from 'electron';
|
||||
import {join} from 'path';
|
||||
import {URL} from 'url';
|
||||
import * as os from 'os';
|
||||
|
||||
async function createWindow() {
|
||||
const browserWindowConstructorOptions: BrowserWindowConstructorOptions = {
|
||||
show: false, // Use 'ready-to-show' event to show window
|
||||
width: 1050,
|
||||
minWidth: 640,
|
||||
minHeight: 600,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nativeWindowOpen: true,
|
||||
webviewTag: false, // The webview tag is not recommended. Consider alternatives like iframe or Electron's BrowserView. https://www.electronjs.org/docs/latest/api/webview-tag#warning
|
||||
preload: join(__dirname, '../../preload/dist/index.cjs'),
|
||||
},
|
||||
}
|
||||
const osType = os.type();
|
||||
if (osType === 'Darwin') {
|
||||
browserWindowConstructorOptions.titleBarStyle = "hiddenInset";
|
||||
}
|
||||
const browserWindow = new BrowserWindow(browserWindowConstructorOptions);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('SEND SEND SEND message');
|
||||
browserWindow.webContents.send("container-stopped-event", "containerID");
|
||||
}, 5000);
|
||||
|
||||
|
||||
ipcMain.on("container-stopped-event", (event: any, info: any) => {
|
||||
console.log('SEND SEND SEND message', info);
|
||||
browserWindow.webContents.send("container-stopped-event", event);
|
||||
});
|
||||
|
||||
/**
|
||||
* If you install `show: true` then it can cause issues when trying to close the window.
|
||||
* Use `show: false` and listener events `ready-to-show` to fix these issues.
|
||||
*
|
||||
* @see https://github.com/electron/electron/issues/25012
|
||||
*/
|
||||
browserWindow.on('ready-to-show', () => {
|
||||
browserWindow?.show();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
browserWindow?.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* URL for main window.
|
||||
* Vite dev server for development.
|
||||
* `file://../renderer/index.html` for production and test
|
||||
*/
|
||||
const pageUrl = import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
|
||||
? import.meta.env.VITE_DEV_SERVER_URL
|
||||
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
|
||||
|
||||
|
||||
await browserWindow.loadURL(pageUrl);
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore existing BrowserWindow or Create new BrowserWindow
|
||||
*/
|
||||
export async function restoreOrCreateWindow() {
|
||||
let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
|
||||
|
||||
if (window === undefined) {
|
||||
window = await createWindow();
|
||||
}
|
||||
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
window.focus();
|
||||
}
|
||||
145
packages/main/src/security-restrictions.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import {app, shell} from 'electron';
|
||||
import {URL} from 'url';
|
||||
|
||||
/**
|
||||
* List of origins that you allow open INSIDE the application and permissions for each of them.
|
||||
*
|
||||
* In development mode you need allow open `VITE_DEV_SERVER_URL`
|
||||
*/
|
||||
const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<'clipboard-read' | 'media' | 'display-capture' | 'mediaKeySystem' | 'geolocation' | 'notifications' | 'midi' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' | 'unknown'>>(
|
||||
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
|
||||
? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set]]
|
||||
: [],
|
||||
);
|
||||
|
||||
/**
|
||||
* List of origins that you allow open IN BROWSER.
|
||||
* Navigation to origins below is possible only if the link opens in a new window
|
||||
*
|
||||
* @example
|
||||
* <a
|
||||
* target="_blank"
|
||||
* href="https://github.com/"
|
||||
* >
|
||||
*/
|
||||
const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>([
|
||||
'https://github.com',
|
||||
]);
|
||||
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
|
||||
/**
|
||||
* Block navigation to origins not on the allowlist.
|
||||
*
|
||||
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
|
||||
* from its current page, they can possibly force the app to open web sites on the Internet.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||
*/
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
const {origin} = new URL(url);
|
||||
if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('Blocked navigating to an unallowed origin:', origin);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Block requested unallowed permissions.
|
||||
* By default, Electron will automatically approve all permission requests.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
|
||||
*/
|
||||
contents.session.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
const {origin} = new URL(webContents.getURL());
|
||||
|
||||
const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission);
|
||||
callback(permissionGranted);
|
||||
|
||||
if (!permissionGranted && import.meta.env.DEV) {
|
||||
console.warn(`${origin} requested permission for '${permission}', but was blocked.`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Hyperlinks to allowed sites open in the default browser.
|
||||
*
|
||||
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
||||
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
||||
* You should deny any unexpected window creation.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||
*/
|
||||
contents.setWindowOpenHandler(({url}) => {
|
||||
const {origin} = new URL(url);
|
||||
|
||||
// @ts-expect-error Type checking is performed in runtime
|
||||
if (ALLOWED_EXTERNAL_ORIGINS.has(origin)) {
|
||||
// Open default browser
|
||||
shell.openExternal(url).catch(console.error);
|
||||
|
||||
} else if (import.meta.env.DEV) {
|
||||
console.warn('Blocked the opening of an unallowed origin:', origin);
|
||||
}
|
||||
|
||||
// Prevent creating new window in application
|
||||
return {action: 'deny'};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Verify webview options before creation
|
||||
*
|
||||
* Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
|
||||
*/
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
const {origin} = new URL(params.src);
|
||||
if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`A webview tried to attach ${params.src}, but was blocked.`);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip away preload scripts if unused or verify their location is legitimate
|
||||
delete webPreferences.preload;
|
||||
// @ts-expect-error `preloadURL` exists - see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview
|
||||
delete webPreferences.preloadURL;
|
||||
|
||||
// Disable Node.js integration
|
||||
webPreferences.nodeIntegration = false;
|
||||
});
|
||||
});
|
||||
BIN
packages/main/src/tray-icon.png
Normal file
|
After Width: | Height: | Size: 407 B |
53
packages/main/src/tray-icon.svg
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
color="#ff0000"
|
||||
viewBox="0 0 143.83803 144.99243"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="tray-icon.svg"
|
||||
width="143.83803"
|
||||
height="144.99243"
|
||||
inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs102" /><sodipodi:namedview
|
||||
id="namedview100"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.6751592"
|
||||
inkscape:cx="14.557192"
|
||||
inkscape:cy="88.839688"
|
||||
inkscape:window-width="2111"
|
||||
inkscape:window-height="895"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><g
|
||||
id="g97"
|
||||
style="stroke:#ffffff;stroke-opacity:1"
|
||||
transform="matrix(0.88091615,0,0,0.88091615,17.79993,37.920435)"><path
|
||||
d="m 48.17,0.36 73.7,13.39 c 0.54,0.1 1,0.45 1,1 V 63.6 c 0,0.55 -0.46,0.91 -1,1 l -73.7,13.47 c -0.54,0.1 -1,-0.45 -1,-1 V 1.36 c 0,-0.55 0.46,-1.1 1,-1 z M 0.69,7.06 10.72,5.39 V 72.98 L 0.69,71.28 C 0.31,71.21 0,70.97 0,70.59 V 7.75 C 0,7.37 0.31,7.13 0.69,7.06 Z M 14.6,4.74 26.04,2.83 V 75.58 L 14.6,73.65 Z M 29.93,2.19 43.04,0.01 c 0.37,-0.06 0.69,0.31 0.69,0.69 v 77.1 c 0,0.38 -0.31,0.75 -0.69,0.69 L 29.93,76.26 V 2.45 Z m 89.41,16.01 v 42.05 h -1.3 V 18.2 Z M 57.63,9.83 V 69.89 H 53.74 V 8.63 Z m 10.65,0.99 V 68.3 H 64.71 V 10.36 Z m 9.53,1.54 V 66.69 H 74.57 V 11.75 Z m 8.5,1.33 V 65.52 H 83.39 V 13.01 Z m 7.84,1.26 v 49.13 h -2.6 V 14.41 Z m 7.23,0.91 V 62.97 H 99.11 V 15.34 Z m 6.48,1.18 v 44.92 h -1.95 V 16.43 Z m 6.04,1.02 v 42.97 h -1.62 V 17.31 Z"
|
||||
id="path95"
|
||||
style="stroke:#ffffff;stroke-opacity:1" /></g><ellipse
|
||||
style="fill:none;fill-opacity:0.117647;stroke:#ffffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2757"
|
||||
cx="71.919014"
|
||||
cy="72.496216"
|
||||
rx="70.419014"
|
||||
ry="70.996216" /></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
69
packages/main/tests/unit.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type {MaybeMocked} from 'vitest';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import {restoreOrCreateWindow} from '../src/mainWindow';
|
||||
|
||||
import {BrowserWindow} from 'electron';
|
||||
|
||||
/**
|
||||
* Mock real electron BrowserWindow API
|
||||
*/
|
||||
vi.mock('electron', () => {
|
||||
|
||||
const bw = vi.fn() as MaybeMocked<typeof BrowserWindow>;
|
||||
// @ts-expect-error It's work in runtime, but I Haven't idea how to fix this type error
|
||||
bw.getAllWindows = vi.fn(() => bw.mock.instances);
|
||||
bw.prototype.loadURL = vi.fn();
|
||||
bw.prototype.on = vi.fn();
|
||||
bw.prototype.destroy = vi.fn();
|
||||
bw.prototype.isDestroyed = vi.fn();
|
||||
bw.prototype.isMinimized = vi.fn();
|
||||
bw.prototype.focus = vi.fn();
|
||||
bw.prototype.restore = vi.fn();
|
||||
|
||||
return {BrowserWindow: bw};
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
test('Should create new window', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
expect(mock.instances).toHaveLength(0);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
expect(mock.instances[0].loadURL).toHaveBeenCalledOnce();
|
||||
expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
|
||||
});
|
||||
|
||||
|
||||
test('Should restore existing window', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
|
||||
// Create Window and minimize it
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
const appWindow = vi.mocked(mock.instances[0]);
|
||||
appWindow.isMinimized.mockReturnValueOnce(true);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
expect(appWindow.restore).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
test('Should create new window if previous was destroyed', async () => {
|
||||
const {mock} = vi.mocked(BrowserWindow);
|
||||
|
||||
// Create Window and destroy it
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(1);
|
||||
const appWindow = vi.mocked(mock.instances[0]);
|
||||
appWindow.isDestroyed.mockReturnValueOnce(true);
|
||||
|
||||
await restoreOrCreateWindow();
|
||||
expect(mock.instances).toHaveLength(2);
|
||||
});
|
||||
28
packages/main/tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"types" : ["node"],
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
64
packages/main/vite.config.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import {node} from '../../.electron-vendors.cache.json';
|
||||
import {join} from 'path';
|
||||
import {builtinModules} from 'module';
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
|
||||
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: process.cwd(),
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: 'inline',
|
||||
target: `node${node}`,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE !== 'development',
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'electron',
|
||||
'electron-devtools-installer',
|
||||
...builtinModules.flatMap(p => [p, `node:${p}`]),
|
||||
],
|
||||
output: {
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
brotliSize: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
packages/preload/exposedInMainWorld.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
interface Window {
|
||||
readonly events: { send: (channel: string, data: string) => void; receive: (channel: string, func: any) => void; };
|
||||
readonly listContainers: () => Promise<import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/container-info").ContainerInfo[]>;
|
||||
readonly listImages: () => Promise<import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/image-info").ImageInfo[]>;
|
||||
readonly startContainer: (engine: string, containerId: string) => Promise<void>;
|
||||
readonly createAndStartContainer: (engine: string, options: import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/container-info").ContainerCreateOptions) => Promise<void>;
|
||||
readonly stopContainer: (engine: string, containerId: string) => Promise<void>;
|
||||
readonly startProviderLifecycle: (providerName: string) => Promise<void>;
|
||||
readonly stopProviderLifecycle: (providerName: string) => Promise<void>;
|
||||
readonly buildImage: (buildDirectory: string, imageName: string, eventCollect: (eventName: string, data: string) => void) => Promise<unknown>;
|
||||
readonly getImageInspect: (engine: string, imageId: string) => Promise<import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/image-inspect-info").ImageInspectInfo>;
|
||||
readonly getProviderInfos: () => Promise<import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/provider-info").ProviderInfo[]>;
|
||||
readonly listExtensions: () => Promise<import("/Users/benoitf/git/benoitf/container-desktop/packages/preload/src/api/extension-info").ExtensionInfo[]>;
|
||||
readonly stopExtension: (extensionId: string) => Promise<void>;
|
||||
readonly startExtension: (extensionId: string) => Promise<void>;
|
||||
readonly openExternal: (link: string) => void;
|
||||
}
|
||||
24
packages/preload/src/api.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type { ContainerInfo } from "./api/container-info";
|
||||
|
||||
export interface RemoteAPI {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
listContainers(options?: {}): Promise<ContainerInfo[]>;
|
||||
}
|
||||
35
packages/preload/src/api/container-info.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type Dockerode from 'dockerode';
|
||||
|
||||
export interface ContainerInfo extends Dockerode.ContainerInfo {
|
||||
engine: string;
|
||||
}
|
||||
|
||||
export interface HostConfig {
|
||||
PortBindings?: any;
|
||||
}
|
||||
|
||||
export interface ContainerCreateOptions {
|
||||
name?: string | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ExposedPorts?: { [port: string]: {} } | undefined;
|
||||
HostConfig?: HostConfig | undefined;
|
||||
Image?: string | undefined;
|
||||
}
|
||||
25
packages/preload/src/api/extension-info.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
export interface ExtensionInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
publisher: string;
|
||||
version: string;
|
||||
state: string;
|
||||
}
|
||||
23
packages/preload/src/api/image-info.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type Dockerode from 'dockerode';
|
||||
|
||||
export interface ImageInfo extends Dockerode.ImageInfo {
|
||||
engine: string;
|
||||
}
|
||||
23
packages/preload/src/api/image-inspect-info.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type Dockerode from 'dockerode';
|
||||
|
||||
export interface ImageInspectInfo extends Dockerode.ImageInspectInfo {
|
||||
engine: string;
|
||||
}
|
||||
28
packages/preload/src/api/provider-info.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
export interface ProviderInfoLifecycle {
|
||||
status: string;
|
||||
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
name: string;
|
||||
connection?: string;
|
||||
lifecycle?: ProviderInfoLifecycle;
|
||||
}
|
||||
58
packages/preload/src/command-registry.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { Disposable } from "./types/disposable";
|
||||
|
||||
export interface CommandHandler {
|
||||
callback: any;
|
||||
thisArg: any;
|
||||
}
|
||||
|
||||
export class CommandRegistry {
|
||||
|
||||
private commands = new Map<string, CommandHandler>();
|
||||
|
||||
registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable {
|
||||
|
||||
if (this.commands.has(command)) {
|
||||
throw new Error(`command '${command}' already exists`);
|
||||
}
|
||||
// keep command
|
||||
this.commands.set(command, {
|
||||
callback,
|
||||
thisArg,
|
||||
});
|
||||
return Disposable.create(() => {
|
||||
this.commands.delete(command);
|
||||
});
|
||||
}
|
||||
|
||||
async executeCommand<T = unknown>(commandId: string, ...args: any[]): Promise<T> {
|
||||
// command is on node world, just execute it
|
||||
if (this.commands.has(commandId)) {
|
||||
const command = this.commands.get(commandId);
|
||||
if (command) {
|
||||
return command.callback.apply(command.thisArg, args);
|
||||
}
|
||||
}
|
||||
|
||||
// should try to execute on client side
|
||||
throw new Error('Unknown command: ' + commandId);
|
||||
}
|
||||
}
|
||||
|
||||
279
packages/preload/src/container-registry.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type * as containerDesktopAPI from '@tmpwip/extension-api';
|
||||
import { Disposable } from './types/disposable';
|
||||
import Dockerode from 'dockerode';
|
||||
import { ContainerCreateOptions, ContainerInfo } from './api/container-info';
|
||||
import { ImageInfo } from './api/image-info';
|
||||
import { ImageInspectInfo } from './api/image-inspect-info';
|
||||
import { ProviderInfo } from './api/provider-info';
|
||||
|
||||
const tar: {pack: (dir: string) => NodeJS.ReadableStream} = require('tar-fs');
|
||||
|
||||
export interface InternalContainerProvider {
|
||||
name: string;
|
||||
connection: string;
|
||||
api: Dockerode;
|
||||
}
|
||||
|
||||
export interface InternalContainerProviderLifecycle {
|
||||
internal: containerDesktopAPI.ContainerProviderLifecycle;
|
||||
status: string;
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class ContainerProviderRegistry {
|
||||
|
||||
constructor(private apiSender: any) {
|
||||
|
||||
}
|
||||
|
||||
private providers: Map<string, containerDesktopAPI.ContainerProvider> = new Map();
|
||||
private providerLifecycles: Map<string, InternalContainerProviderLifecycle> = new Map();
|
||||
private internalProviders: Map<string, InternalContainerProvider> = new Map();
|
||||
|
||||
async registerContainerProviderLifecycle(providerLifecycle: containerDesktopAPI.ContainerProviderLifecycle): Promise<Disposable> {
|
||||
|
||||
const providerName = providerLifecycle.provideName();
|
||||
const internalProviderLifecycle = {
|
||||
internal: providerLifecycle,
|
||||
status: providerLifecycle.status(),
|
||||
};
|
||||
this.providerLifecycles.set(providerName, internalProviderLifecycle);
|
||||
return Disposable.create(() => {
|
||||
this.internalProviders.delete(providerName);
|
||||
this.providers.delete(providerName);
|
||||
this.apiSender.send('provider-lifecycle-change', {});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
async registerContainerProvider(provider: containerDesktopAPI.ContainerProvider): Promise<Disposable> {
|
||||
|
||||
const providerName = provider.provideName();
|
||||
const connection = await provider.provideConnection();
|
||||
|
||||
this.providers.set(providerName, provider);
|
||||
|
||||
const internalProvider: InternalContainerProvider = {
|
||||
name: providerName,
|
||||
connection,
|
||||
api: new Dockerode({socketPath: connection}),
|
||||
};
|
||||
|
||||
this.internalProviders.set(providerName, internalProvider);
|
||||
this.apiSender.send('provider-change', {});
|
||||
|
||||
// listen to events
|
||||
internalProvider.api.getEvents((err, stream) => {
|
||||
console.log('error is', err);
|
||||
stream?.on('data', (data) => {
|
||||
const evt = JSON.parse(data.toString());
|
||||
console.log('event is', evt);
|
||||
if (evt.status === 'stop') {
|
||||
// need to notify that a container has been stopped
|
||||
this.apiSender.send('container-stopped-event', evt.id);
|
||||
} else if (evt.status === 'start') {
|
||||
// need to notify that a container has been started
|
||||
this.apiSender.send('container-started-event', evt.id);
|
||||
} else if (evt.status === 'destroy') {
|
||||
// need to notify that a container has been destroyed
|
||||
this.apiSender.send('container-stopped-event', evt.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Disposable.create(() => {
|
||||
this.internalProviders.delete(providerName);
|
||||
this.providers.delete(providerName);
|
||||
this.apiSender.send('provider-change', {});
|
||||
});
|
||||
}
|
||||
|
||||
async listContainers(): Promise<ContainerInfo[]> {
|
||||
const containers = await Promise.all(Array.from(this.internalProviders.values()).map(async (provider) => {
|
||||
try {
|
||||
const containers = await provider.api.listContainers({all:true});
|
||||
return containers.map((container) => {
|
||||
const containerInfo: ContainerInfo = {...container,
|
||||
engine: provider.name,
|
||||
};
|
||||
return containerInfo;
|
||||
});} catch (error) {
|
||||
console.log('error in engine', provider.name, error);
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
const flatttenedContainers = containers.flat();
|
||||
return flatttenedContainers;
|
||||
}
|
||||
|
||||
async listImages(): Promise<ImageInfo[]> {
|
||||
const images = await Promise.all(Array.from(this.internalProviders.values()).map(async (provider) => {
|
||||
try {
|
||||
const images = await provider.api.listImages({all:true});
|
||||
return images.map((image) => {
|
||||
const imageInfo: ImageInfo = {...image,
|
||||
engine: provider.name,
|
||||
};
|
||||
return imageInfo;
|
||||
});} catch (error) {
|
||||
console.log('error in engine', provider.name, error);
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
const flatttenedImages = images.flat();
|
||||
return flatttenedImages;
|
||||
}
|
||||
|
||||
async startProviderLifecycle(providerName: string): Promise<void> {
|
||||
// need to find the container engine of the container
|
||||
const providerLifecycle = this.providerLifecycles.get(providerName);
|
||||
if (!providerLifecycle) {
|
||||
throw new Error('no provider matching this providerName');
|
||||
}
|
||||
await providerLifecycle.internal.start();
|
||||
this.apiSender.send('provider-lifecycle-change', {});
|
||||
}
|
||||
|
||||
async stopProviderLifecycle(providerName: string): Promise<void> {
|
||||
// need to find the container engine of the container
|
||||
const providerLifecycle = this.providerLifecycles.get(providerName);
|
||||
if (!providerLifecycle) {
|
||||
throw new Error('no provider matching this providerName');
|
||||
}
|
||||
await providerLifecycle.internal.stop();
|
||||
this.apiSender.send('provider-lifecycle-change', {});
|
||||
}
|
||||
|
||||
async stopContainer(engineName: string, id: string): Promise<void> {
|
||||
// need to find the container engine of the container
|
||||
const engine = this.internalProviders.get(engineName);
|
||||
if (!engine) {
|
||||
throw new Error('no engine matching this container');
|
||||
}
|
||||
return engine.api.getContainer(id).stop();
|
||||
}
|
||||
|
||||
async startContainer(engineName: string, id: string): Promise<void> {
|
||||
// need to find the container engine of the container
|
||||
const engine = this.internalProviders.get(engineName);
|
||||
if (!engine) {
|
||||
throw new Error('no engine matching this container');
|
||||
}
|
||||
return engine.api.getContainer(id).start();
|
||||
}
|
||||
|
||||
async createAndStartContainer(engineName: string, options: ContainerCreateOptions): Promise<void> {
|
||||
// need to find the container engine of the container
|
||||
const engine = this.internalProviders.get(engineName);
|
||||
if (!engine) {
|
||||
throw new Error('no engine matching this container');
|
||||
}
|
||||
const container = await engine.api.createContainer(options);
|
||||
return container.start();
|
||||
}
|
||||
|
||||
async getImageInspect(engineName: string, id: string): Promise<ImageInspectInfo> {
|
||||
// need to find the container engine of the container
|
||||
const engine = this.internalProviders.get(engineName);
|
||||
if (!engine) {
|
||||
throw new Error('no engine matching this container');
|
||||
}
|
||||
const imageObject = engine.api.getImage(id);
|
||||
const imageInspect = await imageObject.inspect();
|
||||
return {
|
||||
engine: engineName,
|
||||
...imageInspect,
|
||||
};
|
||||
}
|
||||
|
||||
async buildImage(rootDirectory: string, imageName: string, eventCollect: (eventName: string, data: string) => void): Promise<unknown> {
|
||||
console.log('building image', imageName, 'from rootDirectory', rootDirectory);
|
||||
const firstProvider = Array.from(this.internalProviders.values())[0];
|
||||
|
||||
// const firstProvider = this.internalProviders.get('Lima')!;
|
||||
|
||||
console.log('building using provider', firstProvider.name);
|
||||
|
||||
const tarStream = tar.pack(rootDirectory);
|
||||
const streamingPromise = await firstProvider.api.buildImage(tarStream, {t: imageName});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
let resolve: (output: {}) => void;
|
||||
let reject: (err: Error) => void;
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function onFinished(err: Error| null, output: {}) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(output);
|
||||
}
|
||||
|
||||
function onProgress(
|
||||
event: {stream?: string; status?: string; progress?: string;}) {
|
||||
if (event.stream) {
|
||||
eventCollect('stream', event.stream);
|
||||
}
|
||||
// console.log('status=>',event.status);
|
||||
// console.log('progress=>',event.progress);
|
||||
}
|
||||
|
||||
firstProvider.api.modem.followProgress(streamingPromise, onFinished, onProgress);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async getProviderInfos(): Promise<ProviderInfo[]> {
|
||||
|
||||
// get unique keys
|
||||
const lifecycleKeys = Array.from(this.providerLifecycles.keys());
|
||||
const providerKeys = Array.from(this.providers.keys());
|
||||
|
||||
// get unique set
|
||||
const uniqueKeys = Array.from(new Set([...lifecycleKeys, ...providerKeys]));
|
||||
|
||||
return uniqueKeys.map((key) => {
|
||||
|
||||
// matching provider ?
|
||||
const internalProvider = this.internalProviders.get(key);
|
||||
const internalProviderLifecycle = this.providerLifecycles.get(key);
|
||||
|
||||
let lifecycle;
|
||||
if (internalProviderLifecycle) {
|
||||
lifecycle = {
|
||||
status: internalProviderLifecycle.status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: key,
|
||||
connection: internalProvider?.connection,
|
||||
lifecycle,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
331
packages/preload/src/extension-loader.ts
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type * as containerDesktopAPI from '@tmpwip/extension-api';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import type { CommandRegistry } from './command-registry';
|
||||
import type { ContainerProviderRegistry } from './container-registry';
|
||||
import type { ExtensionInfo } from './api/extension-info';
|
||||
import * as zipper from 'zip-local';
|
||||
|
||||
/**
|
||||
* Handle the loading of an extension
|
||||
*/
|
||||
|
||||
export interface AnalyzedExtension {
|
||||
id: string,
|
||||
// root folder (where is package.json)
|
||||
path: string,
|
||||
manifest: any,
|
||||
// main entry
|
||||
mainPath: string,
|
||||
api: typeof containerDesktopAPI,
|
||||
}
|
||||
|
||||
export interface ActivatedExtension {
|
||||
id: string;
|
||||
deactivateFunction: any;
|
||||
extensionContext: containerDesktopAPI.ExtensionContext
|
||||
}
|
||||
|
||||
export class ExtensionLoader {
|
||||
|
||||
private overrideRequireDone = false;
|
||||
|
||||
private activatedExtensions = new Map<string, ActivatedExtension>();
|
||||
private analyzedExtensions = new Map<string, AnalyzedExtension>();
|
||||
|
||||
constructor(
|
||||
private commandRegistry: CommandRegistry,
|
||||
private containerProviderRegistry: ContainerProviderRegistry,
|
||||
private apiSender: any,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
async listExtensions(): Promise<ExtensionInfo[]> {
|
||||
return Array.from(this.analyzedExtensions.values()).map(extension => ({
|
||||
name: extension.manifest.name,
|
||||
version: extension.manifest.version,
|
||||
publisher: extension.manifest.publisher,
|
||||
state: this.activatedExtensions.get(extension.id) ? 'active' : 'inactive',
|
||||
id: extension.id,
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
protected overrideRequire() {
|
||||
if (!this.overrideRequireDone) {
|
||||
this.overrideRequireDone = true;
|
||||
const module = require('module');
|
||||
// save original load method
|
||||
const internalLoad = module._load;
|
||||
const analyzedExtensions = this.analyzedExtensions;
|
||||
|
||||
// if we try to resolve theia module, return the filename entry to use cache.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
module._load = function (request: string, parent: any): any {
|
||||
if (request !== '@tmpwip/extension-api') {
|
||||
console.log('loading...', request, parent);
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
return internalLoad.apply(this, arguments);
|
||||
}
|
||||
console.log('in require function, loading with parent', parent.filename);
|
||||
|
||||
const extension = Array.from(analyzedExtensions.values()).find(extension => path.normalize(parent.filename).startsWith(path.normalize((extension.path))));
|
||||
if (extension && extension.api) {
|
||||
return extension.api;
|
||||
}
|
||||
throw new Error('Unable to find extension API');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async loadPackagedFile(filePath: string): Promise<void> {
|
||||
// need to unpack the file before load it
|
||||
console.log('loadPackagedFile', filePath);
|
||||
|
||||
const filename = path.basename(filePath);
|
||||
const dirname = path.dirname(filePath);
|
||||
|
||||
const unpackedDirectory = path.resolve(dirname, `../unpacked/${filename}`);
|
||||
console.log('unpackedDirectory', unpackedDirectory);
|
||||
fs.mkdirSync(unpackedDirectory, {recursive: true});
|
||||
// extract to an existing directory
|
||||
zipper.sync.unzip(filePath).save(unpackedDirectory);
|
||||
|
||||
await this.loadExtension(unpackedDirectory);
|
||||
this.apiSender.send('extension-started', {});
|
||||
}
|
||||
|
||||
|
||||
async start() {
|
||||
|
||||
// add watcher to the $HOME/container-desktop
|
||||
const pluginsDirectory = path.resolve(os.homedir(), '.local/share/container-desktop/plugins');
|
||||
if (fs.existsSync(pluginsDirectory)) {
|
||||
// add watcher
|
||||
fs.watch(pluginsDirectory, (_, filename) => {
|
||||
// need to load the file
|
||||
const packagedFile = path.resolve(pluginsDirectory, filename);
|
||||
setTimeout(() => this.loadPackagedFile(packagedFile), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
let folders;
|
||||
// scan all extensions that we can find from the extensions folder
|
||||
if (import.meta.env.PROD) {
|
||||
console.log('IN PRODUCTION MODE');
|
||||
// in production mode, use the extensions locally
|
||||
console.log('dirname is', __dirname);
|
||||
folders = await this.readProductionFolders(path.join(__dirname, '../../../extensions'));
|
||||
} else {
|
||||
// in development mode, use the extensions locally
|
||||
folders = await this.readDevelopmentFolders(path.join(__dirname, '../../../extensions'));
|
||||
}
|
||||
console.log('found folders', folders);
|
||||
|
||||
// ok now load all extensions from these folders
|
||||
await Promise.all(folders.map(folder => this.loadExtension(folder)));
|
||||
|
||||
}
|
||||
|
||||
async readDevelopmentFolders(path: string): Promise<string[]> {
|
||||
const entries = await fs.promises.readdir(path, {withFileTypes: true});
|
||||
return entries.filter(entry => entry.isDirectory).map(directory => path + '/' + directory.name).filter(item => !item.includes('docker')).filter(item => !item.includes('lima'));
|
||||
}
|
||||
|
||||
|
||||
async readProductionFolders(path: string): Promise<string[]> {
|
||||
const entries = await fs.promises.readdir(path, {withFileTypes: true});
|
||||
return entries.filter(entry => entry.isDirectory).map(directory => path + '/' + directory.name + `/builtin/${directory.name}.cdix`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async loadExtension(extensionPath: string): Promise<void> {
|
||||
// load manifest
|
||||
const manifest = await this.loadManifest(extensionPath);
|
||||
console.log('manifest is', manifest);
|
||||
|
||||
console.log('overriding require');
|
||||
this.overrideRequire();
|
||||
|
||||
// create api object
|
||||
console.log('create API object');
|
||||
const api = this.createApi();
|
||||
|
||||
const extension: AnalyzedExtension = {
|
||||
id: manifest.name,
|
||||
manifest,
|
||||
path: extensionPath,
|
||||
mainPath: path.resolve(extensionPath, manifest.main),
|
||||
api,
|
||||
};
|
||||
|
||||
this.analyzedExtensions.set(extension.id, extension);
|
||||
|
||||
console.log('load runtime...' + extension.mainPath);
|
||||
const runtime = this.loadRuntime(extension.mainPath);
|
||||
|
||||
console.log('Activate extension...');
|
||||
return this.activateExtension(extension, runtime);
|
||||
}
|
||||
|
||||
createApi(): typeof containerDesktopAPI {
|
||||
const commandRegistry = this.commandRegistry;
|
||||
const commands: typeof containerDesktopAPI.commands = {
|
||||
registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): containerDesktopAPI.Disposable {
|
||||
return commandRegistry.registerCommand(command, callback, thisArg);
|
||||
},
|
||||
executeCommand<T = unknown>(commandId: string, ...args: any[]): PromiseLike<T> {
|
||||
return commandRegistry.executeCommand(commandId, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
//export function executeCommand<T = unknown>(command: string, ...rest: any[]): PromiseLike<T>;
|
||||
|
||||
const containerProviderRegistry = this.containerProviderRegistry;
|
||||
const container: typeof containerDesktopAPI.container = {
|
||||
async registerContainerProvider(provider: containerDesktopAPI.ContainerProvider): Promise<containerDesktopAPI.Disposable> {
|
||||
return containerProviderRegistry.registerContainerProvider(provider);
|
||||
},
|
||||
async registerContainerProviderLifecycle(providerLifecycle: containerDesktopAPI.ContainerProviderLifecycle): Promise<containerDesktopAPI.Disposable> {
|
||||
return containerProviderRegistry.registerContainerProviderLifecycle(providerLifecycle);
|
||||
},
|
||||
};
|
||||
/*
|
||||
export namespace container {
|
||||
export function registerContainerProvider(provider: ContainerProvider): Disposable;
|
||||
}*/
|
||||
|
||||
return <typeof containerDesktopAPI>{
|
||||
commands,
|
||||
container,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
loadRuntime(extensionPathFolder: string): NodeRequire {
|
||||
// cleaning the cache for all files of that plug-in.
|
||||
Object.keys(require.cache).forEach(function (key): void {
|
||||
const mod: NodeJS.Module | undefined = require.cache[key];
|
||||
|
||||
// attempting to reload a native module will throw an error, so skip them
|
||||
if (mod?.id.endsWith('.node')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove children that are part of the plug-in
|
||||
let i = mod?.children.length || 0;
|
||||
while (i--) {
|
||||
const childMod: NodeJS.Module | undefined = mod?.children[i];
|
||||
// ensure the child module is not null, is in the plug-in folder, and is not a native module (see above)
|
||||
if (childMod && childMod.id.startsWith(extensionPathFolder) && !childMod.id.endsWith('.node')) {
|
||||
// cleanup exports - note that some modules (e.g. ansi-styles) define their
|
||||
// exports in an immutable manner, so overwriting the exports throws an error
|
||||
delete childMod.exports;
|
||||
mod?.children.splice(i, 1);
|
||||
for (let j = 0; j < childMod.children.length; j++) {
|
||||
delete childMod.children[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key.startsWith(extensionPathFolder)) {
|
||||
// delete entry
|
||||
delete require.cache[key];
|
||||
const ix = mod?.parent?.children.indexOf(mod) || 0;
|
||||
if (ix >= 0) {
|
||||
mod?.parent?.children.splice(ix, 1);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
return require(extensionPathFolder);
|
||||
}
|
||||
|
||||
|
||||
async loadManifest(extensionPath: string): Promise<any> {
|
||||
const manifestPath = path.join(extensionPath, 'package.json');
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(manifestPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(JSON.parse(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async activateExtension(extension: AnalyzedExtension, extensionMain: any): Promise<void> {
|
||||
const subscriptions: containerDesktopAPI.Disposable[] = [];
|
||||
|
||||
const extensionContext: containerDesktopAPI.ExtensionContext = {
|
||||
subscriptions,
|
||||
};
|
||||
let deactivateFunction = undefined;
|
||||
if (typeof extensionMain['deactivate'] === 'function') {
|
||||
deactivateFunction = extensionMain['deactivate'];
|
||||
}
|
||||
if (typeof extensionMain['activate'] === 'function') {
|
||||
// return exports
|
||||
await extensionMain['activate'].apply(undefined, [extensionContext]);
|
||||
}
|
||||
const id = extension.id;
|
||||
const activatedExtension: ActivatedExtension = {
|
||||
id,
|
||||
deactivateFunction,
|
||||
extensionContext,
|
||||
};
|
||||
this.activatedExtensions.set(extension.id, activatedExtension);
|
||||
}
|
||||
|
||||
async deactivateExtension(extensionId: string): Promise<void> {
|
||||
const extension = this.activatedExtensions.get(extensionId);
|
||||
if (extension) {
|
||||
if (extension.deactivateFunction) {
|
||||
await extension.deactivateFunction();
|
||||
}
|
||||
|
||||
// dispose subscriptions
|
||||
extension.extensionContext.subscriptions.forEach((subscription) => {
|
||||
subscription.dispose();
|
||||
});
|
||||
|
||||
this.activatedExtensions.delete(extensionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async stopAllExtensions(): Promise<void> {
|
||||
await Promise.all(Array.from(this.activatedExtensions.keys()).map((extensionId) => this.deactivateExtension(extensionId)));
|
||||
}
|
||||
|
||||
async startExtension(extensionId: string): Promise<void> {
|
||||
const extension = this.analyzedExtensions.get(extensionId);
|
||||
if (extension) {
|
||||
await this.loadExtension(extension?.path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
packages/preload/src/index.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
/**
|
||||
* @module preload
|
||||
*/
|
||||
|
||||
import {contextBridge} from 'electron';
|
||||
import type { ContainerCreateOptions, ContainerInfo } from './api/container-info';
|
||||
import type { ExtensionInfo } from './api/extension-info';
|
||||
import { CommandRegistry } from './command-registry';
|
||||
import { ContainerProviderRegistry } from './container-registry';
|
||||
import { ExtensionLoader } from './extension-loader';
|
||||
import EventEmitter from 'events';
|
||||
import type { ImageInfo } from './api/image-info';
|
||||
import { ImageInspectInfo } from './api/image-inspect-info';
|
||||
import { ProviderInfo } from './api/provider-info';
|
||||
const shell = require('electron').shell;
|
||||
|
||||
// initialize extension loader mechanism
|
||||
function initExtensions(): void {
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
const apiSender = {
|
||||
send: (channel: string, data: string) => {
|
||||
eventEmitter.emit(channel, data);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
receive: (channel: string, func: any) => {
|
||||
eventEmitter.on(channel, (data) => {
|
||||
func(data);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('events', apiSender);
|
||||
|
||||
const commandRegistry = new CommandRegistry();
|
||||
const containerProviderRegistry = new ContainerProviderRegistry(apiSender);
|
||||
|
||||
contextBridge.exposeInMainWorld('listContainers', async (): Promise<ContainerInfo[]> => {
|
||||
return containerProviderRegistry.listContainers();
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('listImages', async (): Promise<ImageInfo[]> => {
|
||||
return containerProviderRegistry.listImages();
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('startContainer', async (engine: string, containerId: string): Promise<void> => {
|
||||
return containerProviderRegistry.startContainer(engine, containerId);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('createAndStartContainer', async (engine: string, options: ContainerCreateOptions): Promise<void> => {
|
||||
return containerProviderRegistry.createAndStartContainer(engine, options);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('stopContainer', async (engine: string, containerId: string): Promise<void> => {
|
||||
return containerProviderRegistry.stopContainer(engine, containerId);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('startProviderLifecycle', async (providerName: string): Promise<void> => {
|
||||
return containerProviderRegistry.startProviderLifecycle(providerName);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('stopProviderLifecycle', async (providerName: string): Promise<void> => {
|
||||
return containerProviderRegistry.stopProviderLifecycle(providerName);
|
||||
});
|
||||
|
||||
|
||||
contextBridge.exposeInMainWorld('buildImage', async (buildDirectory: string, imageName: string, eventCollect: (eventName: string, data: string) => void): Promise<unknown> => {
|
||||
return containerProviderRegistry.buildImage(buildDirectory, imageName, eventCollect);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('getImageInspect', async (engine: string, imageId: string): Promise<ImageInspectInfo> => {
|
||||
return containerProviderRegistry.getImageInspect(engine, imageId);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('getProviderInfos', (): Promise<ProviderInfo[]> => {
|
||||
return containerProviderRegistry.getProviderInfos();
|
||||
});
|
||||
|
||||
const extensionLoader = new ExtensionLoader(commandRegistry, containerProviderRegistry, apiSender);
|
||||
contextBridge.exposeInMainWorld('listExtensions', async (): Promise<ExtensionInfo[]> => {
|
||||
return extensionLoader.listExtensions();
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('stopExtension', async (extensionId: string): Promise<void> => {
|
||||
return extensionLoader.deactivateExtension(extensionId);
|
||||
});
|
||||
contextBridge.exposeInMainWorld('startExtension', async (extensionId: string): Promise<void> => {
|
||||
return extensionLoader.startExtension(extensionId);
|
||||
});
|
||||
|
||||
|
||||
contextBridge.exposeInMainWorld('openExternal', (link:string): void => {
|
||||
shell.openExternal(link);
|
||||
});
|
||||
|
||||
extensionLoader.start();
|
||||
|
||||
|
||||
}
|
||||
|
||||
// start extensions
|
||||
initExtensions();
|
||||
51
packages/preload/src/types/disposable.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
export class Disposable {
|
||||
private disposable: undefined | (() => void);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static from(...disposables: { dispose(): any }[]): Disposable {
|
||||
return new Disposable(() => {
|
||||
if (disposables) {
|
||||
for (const disposable of disposables) {
|
||||
if (disposable && typeof disposable.dispose === 'function') {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor(func: () => void) {
|
||||
this.disposable = func;
|
||||
}
|
||||
/**
|
||||
* Dispose this object.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.disposable) {
|
||||
this.disposable();
|
||||
this.disposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static create(func: () => void): Disposable {
|
||||
return new Disposable(func);
|
||||
}
|
||||
}
|
||||
30
packages/preload/tsconfig.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"types" : ["node"],
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"exposedInMainWorld.d.ts",
|
||||
"../../types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
70
packages/preload/vite.config.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import {chrome} from '../../.electron-vendors.cache.json';
|
||||
import {join} from 'path';
|
||||
import {builtinModules} from 'module';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
|
||||
/**
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitejs.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
envDir: process.cwd(),
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
},
|
||||
},
|
||||
/*plugins: [
|
||||
commonjs({
|
||||
dynamicRequireTargets: [
|
||||
// include using a glob pattern (either a string or an array of strings)
|
||||
'node_modules/ssh2/lib/protocol/crypto/poly1305.js',
|
||||
]
|
||||
}),
|
||||
],*/
|
||||
build: {
|
||||
sourcemap: 'inline',
|
||||
target: `chrome${chrome}`,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: process.env.MODE !== 'development',
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'electron',
|
||||
...builtinModules.flatMap(p => [p, `node:${p}`]),
|
||||
],
|
||||
output: {
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
brotliSize: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
13
packages/renderer/.eslintrc.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": false
|
||||
},
|
||||
"extends": [
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
1
packages/renderer/assets/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-10 h-10 text-indigo-300 p-1 rounded-full bg-indigo-900" viewBox="0 0 122.88 78.5" style="enable-background:new 0 0 122.88 78.5" xml:space="preserve"><g><path d="M48.17,0.36l73.7,13.39c0.54,0.1,1,0.45,1,1V63.6c0,0.55-0.46,0.91-1,1l-73.7,13.47c-0.54,0.1-1-0.45-1-1V1.36 C47.17,0.81,47.63,0.26,48.17,0.36L48.17,0.36z M0.69,7.06l10.03-1.67v67.59L0.69,71.28C0.31,71.21,0,70.97,0,70.59V7.75 C0,7.37,0.31,7.13,0.69,7.06L0.69,7.06z M14.6,4.74l11.44-1.91v72.75L14.6,73.65V4.74L14.6,4.74z M29.93,2.19l13.11-2.18 c0.37-0.06,0.69,0.31,0.69,0.69V77.8c0,0.38-0.31,0.75-0.69,0.69l-13.11-2.23V2.45V2.19L29.93,2.19z M119.34,18.2v42.05h-1.3V18.2 H119.34L119.34,18.2z M57.63,9.83v60.06h-3.89V8.63L57.63,9.83L57.63,9.83z M68.28,10.82V68.3h-3.57V10.36L68.28,10.82L68.28,10.82 z M77.81,12.36v54.33h-3.24V11.75L77.81,12.36L77.81,12.36z M86.31,13.69v51.83h-2.92V13.01L86.31,13.69L86.31,13.69z M94.15,14.95 v49.13h-2.6V14.41L94.15,14.95L94.15,14.95z M101.38,15.86v47.11h-2.27V15.34L101.38,15.86L101.38,15.86z M107.86,17.04v44.92 h-1.95V16.43L107.86,17.04L107.86,17.04z M113.9,18.06v42.97h-1.62V17.31L113.9,18.06L113.9,18.06z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
13
packages/renderer/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="pf-theme-dark" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Container Desktop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
36
packages/renderer/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "renderer",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@patternfly/patternfly": "^4.179.1",
|
||||
"@rollup/plugin-dynamic-import-vars": "^1.4.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.11",
|
||||
"@tsconfig/svelte": "^2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"ninja-keys": "^1.1.12",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-load-config": "^3.1.1",
|
||||
"svelte": "^3.37.0",
|
||||
"svelte-check": "^2.1.0",
|
||||
"svelte-fa": "^2.4.0",
|
||||
"svelte-preprocess": "^4.7.2",
|
||||
"tailwindcss": "^3.0.19",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinro": "^0.6.12"
|
||||
}
|
||||
}
|
||||
4
packages/renderer/src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
/.vscode/
|
||||
.DS_Store
|
||||
314
packages/renderer/src/App.svelte
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
<script lang="ts">
|
||||
import 'ninja-keys';
|
||||
import './app.css'
|
||||
import '@patternfly/patternfly/patternfly.css'
|
||||
import '@patternfly/patternfly/patternfly-addons.css'
|
||||
import '@patternfly/patternfly/patternfly-theme-dark.css'
|
||||
import './override.css'
|
||||
import { Route, router} from 'tinro';
|
||||
|
||||
import {containersInfos} from './stores/containers';
|
||||
import {imagesInfos} from './stores/images';
|
||||
import ContainerList from './lib/ContainerList.svelte';
|
||||
import {CommandRegistry} from './lib/CommandRegistry'
|
||||
import { onMount } from "svelte";
|
||||
import ExtensionList from './lib/ExtensionList.svelte';
|
||||
import ImagesList from './lib/ImagesList.svelte';
|
||||
import ProviderList from './lib/ProviderList.svelte';
|
||||
let containersCountValue;
|
||||
|
||||
router.mode.hash()
|
||||
let innerWidth = 0
|
||||
|
||||
onMount(async () => {
|
||||
const commandRegistry = new CommandRegistry();
|
||||
commandRegistry.init();
|
||||
containersInfos.subscribe(value => {
|
||||
containersCountValue = value.length;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
function jumpToImages() {
|
||||
console.log('click on images...');
|
||||
window.location.href = '#/images';
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<svelte:window bind:innerWidth/>
|
||||
<Route path="/*" breadcrumb="Home" let:meta>
|
||||
<main class="min-h-screen flex flex-col h-screen bg-gray-800">
|
||||
<ninja-keys id="command-palette" placeholder="" openHotkey="F1" hideBreadcrumbs class="dark"></ninja-keys>
|
||||
|
||||
<header id="navbar" class="text-gray-400 bg-zinc-900 body-font" style="-webkit-app-region: drag;">
|
||||
<div class="container flex mx-auto flex-col p-2 items-center">
|
||||
<div class="flex lg:w-1/5 flex-wrap items-center text-base ml-auto">
|
||||
</div>
|
||||
<div class="flex order-none title-font font-medium items-center text-white align-middle justify-center mb-4 md:mb-0">
|
||||
<!--<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-10 h-10 text-white p-2 bg-indigo-500 rounded-full" viewBox="0 0 24 24">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
||||
</svg>-->
|
||||
|
||||
<!--<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-10 h-10 text-indigo-300 p-1 rounded-full bg-indigo-900" viewBox="0 0 122.88 78.5" style="enable-background:new 0 0 122.88 78.5" xml:space="preserve"><g><path d="M48.17,0.36l73.7,13.39c0.54,0.1,1,0.45,1,1V63.6c0,0.55-0.46,0.91-1,1l-73.7,13.47c-0.54,0.1-1-0.45-1-1V1.36 C47.17,0.81,47.63,0.26,48.17,0.36L48.17,0.36z M0.69,7.06l10.03-1.67v67.59L0.69,71.28C0.31,71.21,0,70.97,0,70.59V7.75 C0,7.37,0.31,7.13,0.69,7.06L0.69,7.06z M14.6,4.74l11.44-1.91v72.75L14.6,73.65V4.74L14.6,4.74z M29.93,2.19l13.11-2.18 c0.37-0.06,0.69,0.31,0.69,0.69V77.8c0,0.38-0.31,0.75-0.69,0.69l-13.11-2.23V2.45V2.19L29.93,2.19z M119.34,18.2v42.05h-1.3V18.2 H119.34L119.34,18.2z M57.63,9.83v60.06h-3.89V8.63L57.63,9.83L57.63,9.83z M68.28,10.82V68.3h-3.57V10.36L68.28,10.82L68.28,10.82 z M77.81,12.36v54.33h-3.24V11.75L77.81,12.36L77.81,12.36z M86.31,13.69v51.83h-2.92V13.01L86.31,13.69L86.31,13.69z M94.15,14.95 v49.13h-2.6V14.41L94.15,14.95L94.15,14.95z M101.38,15.86v47.11h-2.27V15.34L101.38,15.86L101.38,15.86z M107.86,17.04v44.92 h-1.95V16.43L107.86,17.04L107.86,17.04z M113.9,18.06v42.97h-1.62V17.31L113.9,18.06L113.9,18.06z"/></g></svg>-->
|
||||
|
||||
<svg width="33" height="40" viewBox="0 0 33 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 16.5L30.5021 23.5687L15 36.5L0.497832 24.4313L15 16.5Z" fill="#C4C4C4"/>
|
||||
<path d="M15.5022 11.7033L31.0043 18.772L15.5022 31.7033L1.00003 19.6346L15.5022 11.7033Z" fill="#E9E9E9"/>
|
||||
<path d="M15.5022 11.7033L31.0043 18.772L15.5022 31.7033L1.00003 19.6346L15.5022 11.7033Z" fill="#E9E9E9"/>
|
||||
<path d="M15.5022 5.70331L31.0043 12.772L15.5022 25.7033L1.00003 13.6346L15.5022 5.70331Z" fill="#7A59FA"/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-3 text-xl block text-gray-300">Container Desktop</span>
|
||||
</div>
|
||||
<div class="lg:w-2/5 inline-flex lg:justify-end ml-5 lg:ml-0">
|
||||
<!--
|
||||
<a class="mr-5 hover:text-white">First Link</a>
|
||||
<a class="mr-5 hover:text-white">Second Link</a>
|
||||
<a class="mr-5 hover:text-white">Third Link</a>
|
||||
<a class="hover:text-white">Fourth Link</a>-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="overflow-x-hidden flex flex-1">
|
||||
<nav class="pf-c-nav z-0 group w-12 md:w-52 hover:w-52 shadow flex-col justify-between sm:flex transition-all duration-500 ease-in-out" aria-label="Global">
|
||||
<ul class="pf-c-nav__list">
|
||||
<li class="pf-c-nav__item flex w-full justify-between {meta.url === "/containers" || meta.url === '/' ? 'dark:text-white pf-m-current' : 'dark:text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/containers" class="pf-c-nav__link flex items-center align-middle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pt-1 md:hidden icon icon-tabler icon-tabler-grid" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<rect x="4" y="4" width="6" height="6" rx="1"></rect>
|
||||
<rect x="14" y="4" width="6" height="6" rx="1"></rect>
|
||||
<rect x="4" y="14" width="6" height="6" rx="1"></rect>
|
||||
<rect x="14" y="14" width="6" height="6" rx="1"></rect>
|
||||
</svg>
|
||||
<span class="hidden md:block group-hover:block mr-5">Containers</span>
|
||||
|
||||
{#if containersCountValue > 0}
|
||||
{#if innerWidth >= 768}
|
||||
<span class="pf-c-badge pf-m-read hidden group-hover:flex md:flex items-center justify-center">{containersCountValue}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item flex w-full justify-between {meta.url === "/images" ? 'dark:text-white pf-m-current' : 'dark:text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a
|
||||
href="/images"
|
||||
class="pf-c-nav__link"
|
||||
>
|
||||
<svg class="pt-1 md:hidden icon icon-tabler icon-tabler-puzzle" width="18" height="18" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
||||
|
||||
<span class="hidden md:block group-hover:block mr-5">Images</span>
|
||||
{#if innerWidth >= 768}
|
||||
{#if $imagesInfos.length > 0}
|
||||
<span class="pf-c-badge pf-m-read hidden group-hover:flex md:flex items-center justify-center">{$imagesInfos.length}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item flex w-full justify-between {meta.url === "/providers" ? 'dark:text-white pf-m-current' : 'dark:text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/providers" class="pf-c-nav__link">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pt-1 md:hidden icon icon-tabler icon-tabler-compass" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<polyline points="8 16 10 10 16 8 14 14 8 16"></polyline>
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>
|
||||
<span class="hidden md:block group-hover:block mr-5">Providers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item flex w-full justify-between {meta.url === "/extensions" ? 'dark:text-white pf-m-current' : 'dark:text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/extensions" class="pf-c-nav__link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pt-1 md:hidden icon icon-tabler icon-tabler-puzzle" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<path d="M4 7h3a1 1 0 0 0 1 -1v-1a2 2 0 0 1 4 0v1a1 1 0 0 0 1 1h3a1 1 0 0 1 1 1v3a1 1 0 0 0 1 1h1a2 2 0 0 1 0 4h-1a1 1 0 0 0 -1 1v3a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-1a2 2 0 0 0 -4 0v1a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1h1a2 2 0 0 0 0 -4h-1a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1"></path>
|
||||
</svg>
|
||||
<span class="hidden md:block group-hover:block mr-5">Extensions</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<!--
|
||||
<div class="z-0 group w-8 md:w-52 hover:w-52 bg-slate-900 shadow flex-col justify-between sm:flex transition-all duration-500 ease-in-out ">
|
||||
<ul class="ml-2 mt-2 mr-2 mb-12">
|
||||
<li class="flex w-full justify-between {meta.url === "/containers" ? 'text-white' : 'text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/containers" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-grid" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<rect x="4" y="4" width="6" height="6" rx="1"></rect>
|
||||
<rect x="14" y="4" width="6" height="6" rx="1"></rect>
|
||||
<rect x="4" y="14" width="6" height="6" rx="1"></rect>
|
||||
<rect x="14" y="14" width="6" height="6" rx="1"></rect>
|
||||
</svg>
|
||||
<span class="text-sm ml-2 hidden md:block group-hover:block">Containers</span>
|
||||
</a>
|
||||
{#if containersCountValue > 0}
|
||||
<div class="py-1 px-3 bg-gray-600 rounded text-gray-300 hidden md:flex items-center justify-center text-xs group-hover:flex">{containersCountValue}</div>
|
||||
{/if}
|
||||
</li>
|
||||
<li on:click="{() => jumpToImages()}" class="flex w-full justify-between {meta.url === "/images" ? 'text-white' : 'text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/images" class="flex items-center">
|
||||
<svg class="icon icon-tabler icon-tabler-puzzle" width="18" height="18" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
||||
|
||||
<span class="text-sm ml-2 hidden md:block group-hover:block">Images</span>
|
||||
</a>
|
||||
{#if $imagesInfos.length > 0}
|
||||
<div class="py-1 px-3 bg-gray-600 rounded text-gray-300 hidden md:flex items-center justify-center text-xs group-hover:flex">{$imagesInfos.length}</div>
|
||||
{/if}
|
||||
|
||||
</li>
|
||||
<li class="flex w-full justify-between {meta.url === "/providers" ? 'text-white' : 'text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/providers" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-compass" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<polyline points="8 16 10 10 16 8 14 14 8 16"></polyline>
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>
|
||||
<span class="text-sm ml-2 hidden md:block">Providers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex w-full justify-between {meta.url === "/extensions" ? 'text-white' : 'text-gray-400'} hover:text-gray-300 cursor-pointer items-center mb-6">
|
||||
<a href="/extensions" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-puzzle" width="18" height="18" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"></path>
|
||||
<path d="M4 7h3a1 1 0 0 0 1 -1v-1a2 2 0 0 1 4 0v1a1 1 0 0 0 1 1h3a1 1 0 0 1 1 1v3a1 1 0 0 0 1 1h1a2 2 0 0 1 0 4h-1a1 1 0 0 0 -1 1v3a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-1a2 2 0 0 0 -4 0v1a1 1 0 0 1 -1 1h-3a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1h1a2 2 0 0 0 0 -4h-1a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1"></path>
|
||||
</svg>
|
||||
<span class="text-sm ml-2 hidden md:block group-hover:block">Extensions</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
-->
|
||||
<div class="w-full h-full bg-zinc-800 flex flex-col">
|
||||
<Route path="/containers">
|
||||
<ContainerList />
|
||||
</Route>
|
||||
<Route path="/images">
|
||||
<ImagesList />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<ContainerList />
|
||||
</Route>
|
||||
<Route path="/extensions">
|
||||
<ExtensionList />
|
||||
</Route>
|
||||
<Route path="/providers">
|
||||
<ProviderList />
|
||||
</Route>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- This is an example component
|
||||
<div class="min-h-screen bg-white border-2 border-gray-700">
|
||||
<nav class="flex flex-col w-64 bg-gray-50">
|
||||
<div class="p-4">
|
||||
<img src="https://tailwindcomponents.com/svg/logo-color.svg" />
|
||||
</div>
|
||||
<ul class="p-2 space-y-2 flex-1 overflow-auto" style="scrollbar-width: thin;">
|
||||
<li>
|
||||
<a href="#" class="flex space-x-2 items-center text-gray-600 p-2 bg-gray-200 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 fill-current" width="24" height="24" viewBox="0 0 24 24"><path d="M20 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z"/></svg>
|
||||
<span class="text-gray-900 hidden lg:block">Containers</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex space-x-2 items-center text-gray-600 p-2 hover:bg-gray-200 rounded-lg hover:text-gray-900">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 fill-current" width="24" height="24" viewBox="0 0 24 24"><path d="M17.997 18h-.998c0-1.552.06-1.775-.88-1.993-1.438-.332-2.797-.645-3.293-1.729-.18-.396-.301-1.048.155-1.907 1.021-1.929 1.277-3.583.702-4.538-.672-1.115-2.707-1.12-3.385.017-.576.968-.316 2.613.713 4.512.465.856.348 1.51.168 1.908-.49 1.089-1.836 1.4-3.262 1.728-.982.227-.92.435-.92 2.002h-.995l-.002-.623c0-1.259.1-1.985 1.588-2.329 1.682-.389 3.344-.736 2.545-2.209-2.366-4.365-.676-6.839 1.865-6.839 2.492 0 4.227 2.383 1.867 6.839-.775 1.464.824 1.812 2.545 2.209 1.49.344 1.589 1.072 1.589 2.333l-.002.619zm4.81-2.214c-1.289-.298-2.489-.559-1.908-1.657 1.77-3.342.47-5.129-1.4-5.129-1.265 0-2.248.817-2.248 2.325 0 1.269.574 2.175.904 2.925h1.048c-.17-.75-1.466-2.562-.766-3.736.412-.692 1.704-.693 2.114-.012.38.631.181 1.812-.534 3.161-.388.733-.28 1.301-.121 1.648.305.666.977.987 1.737 1.208 1.507.441 1.368.042 1.368 1.48h.997l.002-.463c0-.945-.074-1.492-1.193-1.75zm-22.805 2.214h.997c0-1.438-.139-1.039 1.368-1.48.761-.221 1.433-.542 1.737-1.208.159-.348.267-.915-.121-1.648-.715-1.349-.914-2.53-.534-3.161.41-.682 1.702-.681 2.114.012.7 1.175-.596 2.986-.766 3.736h1.048c.33-.75.904-1.656.904-2.925.001-1.509-.982-2.326-2.247-2.326-1.87 0-3.17 1.787-1.4 5.129.581 1.099-.619 1.359-1.908 1.657-1.12.258-1.194.805-1.194 1.751l.002.463z"/></svg>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex space-x-2 items-center text-gray-600 p-2 hover:bg-gray-200 rounded-lg hover:text-gray-900">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 fill-current" width="24" height="24" viewBox="0 0 24 24"><path d="M10 15v-10h-5.033c-2.743 0-4.967 2.239-4.967 5 0 2.762 2.224 5 4.967 5h5.033zm-8-5c0-1.654 1.331-3 2.967-3h3.033v6h-3.033c-1.636 0-2.967-1.346-2.967-3zm22-10v20c-4.839-3.008-8.836-4.491-12-5v-2.014c3.042.438 6.393 1.624 10 3.548v-13.064c-3.622 1.941-6.912 3.099-10 3.544v-2.014c3.124-.498 7.036-1.915 12-5zm-11.993 22.475c-.52-.424-.902-.994-1.095-1.637l-1.151-3.827h-6.146l1.905 5.883c.214.659.828 1.106 1.522 1.106h4.46c.358 0 .677-.225.797-.562.12-.337.015-.713-.263-.939l-.029-.024zm-4.674-.475l-.982-3h1.933l.927 3h-1.878z"/></svg>
|
||||
<span>Announcements</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="#" class="flex space-x-2 items-center text-gray-600 p-2 hover:bg-gray-200 rounded-lg hover:text-gray-900">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 fill-current" width="24" height="24" viewBox="0 0 24 24"><path d="M17.997 18h-.998c0-1.552.06-1.775-.88-1.993-1.438-.332-2.797-.645-3.293-1.729-.18-.396-.301-1.048.155-1.907 1.021-1.929 1.277-3.583.702-4.538-.672-1.115-2.707-1.12-3.385.017-.576.968-.316 2.613.713 4.512.465.856.348 1.51.168 1.908-.49 1.089-1.836 1.4-3.262 1.728-.982.227-.92.435-.92 2.002h-.995l-.002-.623c0-1.259.1-1.985 1.588-2.329 1.682-.389 3.344-.736 2.545-2.209-2.366-4.365-.676-6.839 1.865-6.839 2.492 0 4.227 2.383 1.867 6.839-.775 1.464.824 1.812 2.545 2.209 1.49.344 1.589 1.072 1.589 2.333l-.002.619zm4.81-2.214c-1.289-.298-2.489-.559-1.908-1.657 1.77-3.342.47-5.129-1.4-5.129-1.265 0-2.248.817-2.248 2.325 0 1.269.574 2.175.904 2.925h1.048c-.17-.75-1.466-2.562-.766-3.736.412-.692 1.704-.693 2.114-.012.38.631.181 1.812-.534 3.161-.388.733-.28 1.301-.121 1.648.305.666.977.987 1.737 1.208 1.507.441 1.368.042 1.368 1.48h.997l.002-.463c0-.945-.074-1.492-1.193-1.75zm-22.805 2.214h.997c0-1.438-.139-1.039 1.368-1.48.761-.221 1.433-.542 1.737-1.208.159-.348.267-.915-.121-1.648-.715-1.349-.914-2.53-.534-3.161.41-.682 1.702-.681 2.114.012.7 1.175-.596 2.986-.766 3.736h1.048c.33-.75.904-1.656.904-2.925.001-1.509-.982-2.326-2.247-2.326-1.87 0-3.17 1.787-1.4 5.129.581 1.099-.619 1.359-1.908 1.657-1.12.258-1.194.805-1.194 1.751l.002.463z"/></svg>
|
||||
<span>Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="flex space-x-2 items-center text-gray-600 p-2 hover:bg-gray-200 rounded-lg hover:text-gray-900">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 fill-current" width="24" height="24" viewBox="0 0 24 24"><path d="M10 15v-10h-5.033c-2.743 0-4.967 2.239-4.967 5 0 2.762 2.224 5 4.967 5h5.033zm-8-5c0-1.654 1.331-3 2.967-3h3.033v6h-3.033c-1.636 0-2.967-1.346-2.967-3zm22-10v20c-4.839-3.008-8.836-4.491-12-5v-2.014c3.042.438 6.393 1.624 10 3.548v-13.064c-3.622 1.941-6.912 3.099-10 3.544v-2.014c3.124-.498 7.036-1.915 12-5zm-11.993 22.475c-.52-.424-.902-.994-1.095-1.637l-1.151-3.827h-6.146l1.905 5.883c.214.659.828 1.106 1.522 1.106h4.46c.358 0 .677-.225.797-.562.12-.337.015-.713-.263-.939l-.029-.024zm-4.674-.475l-.982-3h1.933l.927 3h-1.878z"/></svg>
|
||||
<span>Announcements</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="p-2 flex items-center border-t-2 border-gray-300 space-x-4">
|
||||
<div class="relative inline-flex">
|
||||
<span class="inline-flex bg-red-500 w-2 h-2 absolute right-0 bottom-0 rounded-full ring-2 ring-white transform translate-x-1/3 translate-y-1/3"></span>
|
||||
<img class='w-8 h-8 object-cover rounded-full' alt='User avatar' src='https://images.unsplash.com/photo-1477118476589-bff2c5c4cfbb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=200&q=200' />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold tracking-wide text-gray-800">
|
||||
Danimai
|
||||
</h3>
|
||||
<p class="text-sm text-gray-700">
|
||||
view profile
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
</Route>
|
||||
<!--
|
||||
<style>
|
||||
:root {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 16rem;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff3e00;
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
font-weight: 100;
|
||||
line-height: 1.1;
|
||||
margin: 2rem auto;
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 14rem;
|
||||
margin: 1rem auto;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
3
packages/renderer/src/app.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
BIN
packages/renderer/src/assets/svelte.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
71
packages/renderer/src/lib/CommandRegistry.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
export class CommandRegistry {
|
||||
|
||||
init(): void {
|
||||
//eslint-disable-next-line
|
||||
const ninja:any = document.getElementById('command-palette');
|
||||
ninja.data = [
|
||||
{
|
||||
id: 'Projects',
|
||||
title: 'Open Projects',
|
||||
hotkey: 'ctrl+N',
|
||||
icon: 'apps',
|
||||
section: 'Projects',
|
||||
handler: () => {
|
||||
// it's auto register above hotkey with this handler
|
||||
alert('Your logic to handle');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Theme',
|
||||
title: 'Change theme...',
|
||||
icon: 'desktop_windows',
|
||||
children: ['Light Theme', 'Dark Theme', 'System Theme'],
|
||||
hotkey: 'ctrl+T',
|
||||
handler: () => {
|
||||
// open menu if closed. Because you can open directly that menu from it's hotkey
|
||||
ninja.open({ parent: 'Theme' });
|
||||
// if menu opened that prevent it from closing on select that action, no need if you don't have child actions
|
||||
return {keepOpen: true};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Light Theme',
|
||||
title: 'Change theme to Light',
|
||||
icon: 'light_mode',
|
||||
parent: 'Theme',
|
||||
handler: () => {
|
||||
// simple handler
|
||||
document.documentElement.classList.remove('dark');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Dark Theme',
|
||||
title: 'Change theme to Dark',
|
||||
icon: 'dark_mode',
|
||||
parent: 'Theme',
|
||||
handler: () => {
|
||||
// simple handler
|
||||
document.documentElement.classList.add('dark');
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
74
packages/renderer/src/lib/ContainerIcon.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
export let state='';
|
||||
|
||||
interface Color {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
tertiaryColor: string;
|
||||
}
|
||||
|
||||
const greenColors =
|
||||
{
|
||||
primaryColor: '#7BAA80',
|
||||
secondaryColor : '#93C29A',
|
||||
tertiaryColor : '#639667'
|
||||
};
|
||||
|
||||
const grayColors =
|
||||
{
|
||||
primaryColor: '#777A77',
|
||||
secondaryColor : '#B9B9B9',
|
||||
tertiaryColor : '#5A5F5A'
|
||||
};
|
||||
|
||||
|
||||
const redColors =
|
||||
{
|
||||
primaryColor: '#F96947',
|
||||
secondaryColor : '#FA7C5F',
|
||||
tertiaryColor : '#E8563A'
|
||||
};
|
||||
|
||||
|
||||
function getColorForState(): Color {
|
||||
if (state === 'running') {
|
||||
return greenColors;
|
||||
} else if (state === 'created') {
|
||||
return redColors;
|
||||
}
|
||||
return grayColors;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<svg class="w-10 rounded-full" width="53" height="32" viewBox="0 0 53 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_168_1172)">
|
||||
<rect x="4" width="45" height="24" rx="2" fill="{getColorForState().primaryColor}"/>
|
||||
</g>
|
||||
<rect x="16" width="33" height="24" rx="2" fill="{getColorForState().secondaryColor}"/>
|
||||
<path d="M4 19H49V22C49 23.1046 48.1046 24 47 24H6C4.89543 24 4 23.1046 4 22V19Z" fill="{getColorForState().tertiaryColor}"/>
|
||||
<path d="M16 19H49V22C49 23.1046 48.1046 24 47 24H18C16.8954 24 16 23.1046 16 22V19Z" fill="{getColorForState().primaryColor}"/>
|
||||
<path d="M16 19H49V22C49 23.1046 48.1046 24 47 24H18C16.8954 24 16 23.1046 16 22V19Z" fill="{getColorForState().primaryColor}"/>
|
||||
<path d="M16 19H49V22C49 23.1046 48.1046 24 47 24H18C16.8954 24 16 23.1046 16 22V19Z" fill="{getColorForState().primaryColor}"/>
|
||||
<path d="M16 19H49V22C49 23.1046 48.1046 24 47 24H18C16.8954 24 16 23.1046 16 22V19Z" fill="{getColorForState().primaryColor}"/>
|
||||
<path d="M16 19H49V22C49 23.1046 48.1046 24 47 24H18C16.8954 24 16 23.1046 16 22V19Z" fill="{getColorForState().primaryColor}"/>
|
||||
<rect x="8" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<rect x="14" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<rect x="21" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<rect x="28" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<rect x="35" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<rect x="42" y="3" width="4" height="14" rx="2" fill="white" fill-opacity="0.4"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_168_1172" x="0" y="0" width="53" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_168_1172"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_168_1172" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
419
packages/renderer/src/lib/ContainerList.svelte
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
<script lang="ts">
|
||||
import Fa from 'svelte-fa/src/fa.svelte'
|
||||
import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faStopCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { onMount } from "svelte";
|
||||
import {filtered, searchPattern} from '../stores/containers';
|
||||
import {providerInfos} from '../stores/providers';
|
||||
import type { ContainerInfo } from '../../../preload/src/api/container-info';
|
||||
import ContainerIcon from './ContainerIcon.svelte'
|
||||
|
||||
let openChoiceModal = false
|
||||
let fromDockerfileModal = false
|
||||
|
||||
let dockerImageName = 'my-custom-image'
|
||||
let dockerImageProviderName = '';
|
||||
let buildInProgress= false;
|
||||
let buildFinished = false;
|
||||
|
||||
let containers = [];
|
||||
let searchTerm = '';
|
||||
$: searchPattern.set(searchTerm);
|
||||
|
||||
onMount(async () => {
|
||||
filtered.subscribe(value => {
|
||||
containers = value;
|
||||
});
|
||||
|
||||
providerInfos.subscribe(value => {
|
||||
if (value.length > 0) {
|
||||
dockerImageProviderName = value[0].name;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function keydownChoice(e: KeyboardEvent) {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
toggleCreateContainer();
|
||||
}
|
||||
}
|
||||
|
||||
function keydownDockerfileChoice(e: KeyboardEvent) {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
fromDockerfileModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCreateContainer(): void {
|
||||
openChoiceModal = !openChoiceModal;
|
||||
}
|
||||
|
||||
function fromExistingImage(): void {
|
||||
openChoiceModal = false;
|
||||
fromDockerfileModal = false;
|
||||
window.location.href = '#/images'
|
||||
}
|
||||
|
||||
function fromDockerfile(): void {
|
||||
openChoiceModal = false;
|
||||
fromDockerfileModal = true;
|
||||
}
|
||||
|
||||
let buildLog = '';
|
||||
|
||||
function eventCollect(eventName: string, data: string): void {
|
||||
buildLog += `${data}<br/>`;
|
||||
}
|
||||
|
||||
async function buildDockerImage(): Promise<void> {
|
||||
buildLog = '';
|
||||
buildInProgress = true;
|
||||
buildFinished = false;
|
||||
|
||||
const data: any = document.getElementById("dockerImageFolder");
|
||||
if (data && data.files && data.files.length > 0) {
|
||||
const dockerFilePath = Array.from(data.files).map(item => item?.path).find(itemPath => itemPath.endsWith('Dockerfile'));
|
||||
if (dockerFilePath) {
|
||||
|
||||
const rootDirectory = dockerFilePath.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');;
|
||||
await window.buildImage(rootDirectory, dockerImageName, eventCollect);
|
||||
buildFinished = true;
|
||||
window.dispatchEvent(new CustomEvent('image-build', { detail: { name: dockerImageName } }));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getName(containerInfo: ContainerInfo) {
|
||||
return containerInfo.Names[0].replace(/^\//, '');
|
||||
}
|
||||
|
||||
function getState(containerInfo: ContainerInfo):string {
|
||||
return (containerInfo.State || '').toUpperCase();
|
||||
}
|
||||
|
||||
function getImage(containerInfo: ContainerInfo): string {
|
||||
return containerInfo.Image;
|
||||
}
|
||||
|
||||
function getPort(containerInfo: ContainerInfo): string {
|
||||
const ports = containerInfo.Ports?.filter(port => port.PublicPort).map(port => port.PublicPort);
|
||||
|
||||
if (ports && ports.length > 1) {
|
||||
return `PORTS: ${ports.join(', ')}`;
|
||||
} else if (ports && ports.length === 1) {
|
||||
return `PORT: ${ports[0]}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function hasPublicPort(containerInfo: ContainerInfo): boolean {
|
||||
const publicPorts = containerInfo.Ports?.filter(port => port.PublicPort).map(port => port.PublicPort);
|
||||
|
||||
return publicPorts.length > 0;
|
||||
}
|
||||
|
||||
|
||||
function openBrowser(containerInfo: ContainerInfo): void {
|
||||
console.log('container.Ports && container.Ports.length', containerInfo.Ports);
|
||||
console.log('containerInfo.Ports.length', containerInfo.Ports.length);
|
||||
|
||||
const ports = containerInfo.Ports?.filter(port => port.PublicPort).map(port => port.PublicPort);
|
||||
if (ports.length > 0) {
|
||||
const port = ports[0];
|
||||
const url = `http://localhost:${port}`;
|
||||
console.log('opening url', url);
|
||||
window.openExternal(url);
|
||||
}
|
||||
}
|
||||
|
||||
function getEngine(containerInfo: ContainerInfo): string {
|
||||
return containerInfo.engine;
|
||||
}
|
||||
|
||||
async function startContainer(containerInfo: ContainerInfo) {
|
||||
await window.startContainer(containerInfo.engine, containerInfo.Id);
|
||||
console.log('container started');
|
||||
}
|
||||
|
||||
async function stopContainer(containerInfo: ContainerInfo) {
|
||||
await window.stopContainer(containerInfo.engine, containerInfo.Id);
|
||||
console.log('container stopped');
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col" >
|
||||
|
||||
<div class="min-w-full">
|
||||
<div class="flex flex-row">
|
||||
<div class="py-5 px-5 lg:w-[35rem] w-[22rem]">
|
||||
<div class="flex items-center bg-gray-700 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 ml-2 mr-2 " fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input bind:value={searchTerm} type="text" name="containerSearchName" placeholder="Search...."
|
||||
class="w-full py-2 outline-none bg-gray-700">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 justify-end">
|
||||
<div class="py-5 px-5">
|
||||
<button on:click="{() => toggleCreateContainer()}" class="pf-c-button pf-m-primary" type="button">
|
||||
<span class="pf-c-button__icon pf-m-start">
|
||||
<i class="fas fa-plus-circle" aria-hidden="true"></i>
|
||||
</span>
|
||||
Create container
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<table
|
||||
class="pf-c-table pf-m-grid-md"
|
||||
role="grid"
|
||||
aria-label="This is a simple table example"
|
||||
id="table-basic"
|
||||
>
|
||||
|
||||
<svg
|
||||
class="pf-c-spinner"
|
||||
role="progressbar"
|
||||
viewBox="0 0 100 100"
|
||||
aria-label="Loading..."
|
||||
>
|
||||
<circle class="pf-c-spinner__path" cx="50" cy="50" r="45" fill="none" />
|
||||
</svg>
|
||||
<caption>This is the table caption</caption>
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">Repositories</th>
|
||||
<th role="columnheader" scope="col">Branches</th>
|
||||
<th role="columnheader" scope="col">Pull requests</th>
|
||||
<th role="columnheader" scope="col">Workspaces</th>
|
||||
<th role="columnheader" scope="col">Last commit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody role="rowgroup">
|
||||
<tr role="row">
|
||||
<td role="cell" data-label="Repository name">Repository 1</td>
|
||||
<td role="cell" data-label="Branches">10</td>
|
||||
<td role="cell" data-label="Pull requests">25</td>
|
||||
<td role="cell" data-label="Workspaces">5</td>
|
||||
<td role="cell" data-label="Last commit">2 days ago</td>
|
||||
</tr>
|
||||
|
||||
<tr role="row">
|
||||
<td role="cell" data-label="Repository name">Repository 2</td>
|
||||
<td role="cell" data-label="Branches">10</td>
|
||||
<td role="cell" data-label="Pull requests">25</td>
|
||||
<td role="cell" data-label="Workspaces">5</td>
|
||||
<td role="cell" data-label="Last commit">2 days ago</td>
|
||||
</tr>
|
||||
|
||||
<tr role="row">
|
||||
<td role="cell" data-label="Repository name">Repository 3</td>
|
||||
<td role="cell" data-label="Branches">10</td>
|
||||
<td role="cell" data-label="Pull requests">25</td>
|
||||
<td role="cell" data-label="Workspaces">5</td>
|
||||
<td role="cell" data-label="Last commit">2 days ago</td>
|
||||
</tr>
|
||||
|
||||
<tr role="row">
|
||||
<td role="cell" data-label="Repository name">Repository 4</td>
|
||||
<td role="cell" data-label="Branches">10</td>
|
||||
<td role="cell" data-label="Pull requests">25</td>
|
||||
<td role="cell" data-label="Workspaces">5</td>
|
||||
<td role="cell" data-label="Last commit">2 days ago</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
<table class="min-w-full divide-y divide-gray-800" class:hidden="{containers.length === 0}">
|
||||
<!--<thead class="bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">State</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ports</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Engine</th>
|
||||
</tr>
|
||||
</thead>-->
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-200">
|
||||
{#each containers as container}
|
||||
<tr>
|
||||
<td class="px-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-10 py-3">
|
||||
<!--<Fa class="h-10 w-10 rounded-full {getColorForState(container)}" icon={faBox} />-->
|
||||
<ContainerIcon state="{container?.State}"/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="flex flex-row">
|
||||
<div class="text-sm text-gray-200">{getName(container)}</div>
|
||||
<div class="pl-2 text-sm text-violet-400">{getImage(container)}</div>
|
||||
</div>
|
||||
<div class="flex flex-row text-xs font-extra-light text-gray-500">
|
||||
<div>{getState(container)}</div>
|
||||
<div class="px-2 inline-flex text-xs font-extralight rounded-full bg-slate-900 text-slate-400">{getEngine(container)}</div>
|
||||
<div class="pl-2 pr-2">{getPort(container)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 whitespace-nowrap">
|
||||
<div class="flex flex-row justify-end">
|
||||
<button title="Open Browser" on:click={() => openBrowser(container)} hidden class:block="{container?.State === 'running' && hasPublicPort(container)}" ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faExternalLinkSquareAlt} /></button>
|
||||
<button title="Start Container" on:click={() => startContainer(container)} hidden class:block="{container?.State !== 'running'}" ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faPlayCircle} /></button>
|
||||
<button title="Stop Container" on:click={() => stopContainer(container)} hidden class:block="{container?.State === 'running'}" ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faStopCircle} /></button>
|
||||
<!--<button title="Delete Container"><Fa class="cursor-pointer h-10 w-10 rounded-full text-3xl text-sky-800" icon={faTrash} /></button>-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full min-w-full flex flex-col" class:hidden="{containers.length > 0}">
|
||||
|
||||
<div class="pf-c-empty-state h-full">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
|
||||
<h1 class="pf-c-title pf-m-lg">No container</h1>
|
||||
<div
|
||||
class="pf-c-empty-state__body"
|
||||
>No container</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{#if openChoiceModal}
|
||||
<div class="modal z-50 fixed w-full h-full top-0 left-0 flex items-center justify-center p-8 lg:p-0" tabindex={0} autofocus on:keydown={keydownChoice}>
|
||||
<div class="modal-overlay fixed w-full h-full bg-gray-900 opacity-50"></div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="relative px-4 w-full max-w-4xl h-full md:h-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
|
||||
<section class="text-gray-400 bg-gray-700 body-font border-gray-200 border-2">
|
||||
<div class="container px-5 py-24 mx-auto flex flex-wrap">
|
||||
<div class="flex flex-wrap -m-4">
|
||||
<div class="p-4 lg:w-1/2 md:w-full ">
|
||||
<div class="flex border-2 rounded-lg border-gray-800 p-8 sm:flex-row flex-col hover:bg-gray-800 hover:cursor-pointer" on:click="{() => fromDockerfile()}">
|
||||
<div class="w-16 h-16 sm:mr-8 sm:mb-0 mb-4 inline-flex items-center justify-center rounded-full bg-gray-800 text-indigo-400 flex-shrink-0">
|
||||
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-8 h-8" viewBox="0 0 24 24">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-white text-lg title-font font-medium mb-3">From Dockerfile</h2>
|
||||
<p class="leading-relaxed text-base">Build image using a local Dockerfile.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 lg:w-1/2 md:w-full">
|
||||
<div class="flex border-2 rounded-lg border-gray-800 p-8 sm:flex-row flex-col hover:bg-gray-800 hover:cursor-pointer" on:click="{() => fromExistingImage()}">
|
||||
<div class="w-16 h-16 sm:mr-8 sm:mb-0 mb-4 inline-flex items-center justify-center rounded-full bg-gray-800 text-indigo-400 flex-shrink-0">
|
||||
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<circle cx="6" cy="6" r="3"></circle>
|
||||
<circle cx="6" cy="18" r="3"></circle>
|
||||
<path d="M20 4L8.12 15.88M14.47 14.48L20 20M8.12 8.12L12 12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-white text-lg title-font font-medium mb-3">From existing image</h2>
|
||||
<p class="leading-relaxed text-base">Use an existing image from the system to build a container.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
{#if fromDockerfileModal}
|
||||
<div class="modal z-50 fixed w-full h-full top-0 left-0 flex items-center justify-center p-8 lg:p-0" tabindex={0} autofocus on:keydown={keydownDockerfileChoice}>
|
||||
<div class="modal-overlay fixed w-full h-full bg-gray-900 opacity-50"></div>
|
||||
|
||||
|
||||
|
||||
<div class="relative px-4 w-full max-w-4xl h-full md:h-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
|
||||
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="flex justify-end p-2">
|
||||
<button on:click='{() => {fromDockerfileModal = false}}' type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white" data-modal-toggle="authentication-modal">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!--<form class="px-6 pb-4 space-y-6 lg:px-8 sm:pb-6 xl:pb-8">-->
|
||||
<div class="px-6 pb-4 space-y-6 lg:px-8 sm:pb-6 xl:pb-8">
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white">Build Image From Dockerfile</h3>
|
||||
<button hidden='{!buildFinished}' on:click="{() => fromExistingImage()}" class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Done</button>
|
||||
|
||||
<div class="flex w-full h-full text-gray-300">
|
||||
{@html buildLog}
|
||||
</div>
|
||||
|
||||
<div hidden='{buildInProgress}'>
|
||||
<label for="dockerImageFolder" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dockerfile base directory</label>
|
||||
<input type="file" webkitdirectory name="dockerImageFolder" id="dockerImageFolder" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required>
|
||||
</div>
|
||||
<div hidden='{buildInProgress}'>
|
||||
<label for="dockerImageName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Image Name</label>
|
||||
<input type="text" bind:value={dockerImageName} name="dockerImageName" id="dockerImageName" placeholder="Enter image name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required>
|
||||
<label for="dockerImageProviderName" class="py-6 block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Provider
|
||||
<select class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" name="providerChoice" bind:value={dockerImageProviderName} >
|
||||
{#each $providerInfos as provider}
|
||||
<option value="{provider.name}">{provider.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<button hidden='{buildInProgress}' on:click="{() => buildDockerImage()}" class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Build</button>
|
||||
<!-- </form>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
74
packages/renderer/src/lib/ExtensionList.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import Fa from 'svelte-fa/src/fa.svelte'
|
||||
import { faPuzzlePiece } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faStopCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { onMount } from "svelte";
|
||||
import {extensionInfos } from '../stores/extensions';
|
||||
import type { ExtensionInfo } from '../../../preload/src/api/extension-info';
|
||||
|
||||
let extensions = [];
|
||||
onMount(async () => {
|
||||
extensionInfos.subscribe(value => {
|
||||
extensions = value;
|
||||
});
|
||||
});
|
||||
|
||||
async function stopExtension(extension: ExtensionInfo) {
|
||||
console.log('stopping extension...');
|
||||
await window.stopExtension(extension.id);
|
||||
window.dispatchEvent(new CustomEvent('extension-stopped', {detail: extension}));
|
||||
}
|
||||
async function startExtension(extension: ExtensionInfo) {
|
||||
console.log('starting extension...');
|
||||
await window.startExtension(extension.id);
|
||||
window.dispatchEvent(new CustomEvent('extension-started', {detail: extension}));
|
||||
console.log('extension started');
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getColorForState(extensionInfo: ExtensionInfo): string {
|
||||
if (extensionInfo.state === 'active') {
|
||||
return 'text-emerald-500'
|
||||
}
|
||||
return 'text-gray-700';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col" >
|
||||
<div class="shadow overflow-hidden border-b border-gray-600 sm">
|
||||
<table class="min-w-full divide-y divide-gray-800">
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-200">
|
||||
{#each extensions as extension}
|
||||
<tr>
|
||||
<td class="px-6 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 py-3">
|
||||
<Fa class="h-10 w-10 rounded-full {getColorForState(extension)}" icon={faPuzzlePiece} />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="flex flex-row">
|
||||
<div class="text-sm text-gray-200">{extension.name}</div>
|
||||
<div class="pl-2 text-sm text-violet-400">{extension.publisher}</div>
|
||||
</div>
|
||||
<div class="flex flex-row text-xs font-extra-light text-gray-500">
|
||||
<div>{extension.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 whitespace-nowrap">
|
||||
<div class="flex flex-row justify-end">
|
||||
<button title="Start extension" on:click={() => startExtension(extension)} hidden class:block="{extension?.state !== 'active'}" ><Fa class="cursor-pointer h-10 w-10 rounded-full text-3xl text-sky-800" icon={faPlayCircle} /></button>
|
||||
<button title="Stop extension" on:click={() => stopExtension(extension)} hidden class:block="{extension?.state === 'active'}" ><Fa class="cursor-pointer h-10 w-10 rounded-full text-3xl text-sky-800" icon={faStopCircle} /></button>
|
||||
<!--<span><Fa class="h-10 w-10 rounded-full text-3xl text-sky-800" icon={faTrash} /></span>-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
264
packages/renderer/src/lib/ImagesList.svelte
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<script lang="ts">
|
||||
import Fa from 'svelte-fa/src/fa.svelte'
|
||||
import { faBox } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faStopCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import {filtered, searchPattern} from '../stores/images'
|
||||
import {searchPattern as searchPatternContainer} from '../stores/containers'
|
||||
import type { ImageInfo } from '../../../preload/src/api/image-info';
|
||||
import type { ImageInspectInfo } from '../../../preload/src/api/image-inspect-info';
|
||||
import type { ContainerCreateOptions } from '../../../preload/src/api/container-info'
|
||||
|
||||
let searchTerm = '';
|
||||
$: searchPattern.set(searchTerm);
|
||||
let runContainerFromImageModal = false
|
||||
let modalImageInspectInfo: ImageInspectInfo;
|
||||
let modalImageInfo: ImageInfo;
|
||||
|
||||
function getName(imageInfo: ImageInfo) {
|
||||
// get name
|
||||
return imageInfo.RepoTags.map((tag => {
|
||||
return tag.split(':')[0];
|
||||
})).join(',');
|
||||
|
||||
}
|
||||
let modalContainerName = '';
|
||||
let modalContainerPortMapping: string[];
|
||||
let modalExposedPorts = [];
|
||||
|
||||
async function runImage(imageInfo: ImageInfo) {
|
||||
modalExposedPorts = [];
|
||||
modalImageInspectInfo = undefined;
|
||||
modalContainerPortMapping = [];
|
||||
modalImageInfo = imageInfo;
|
||||
const imageInspectInfo = await window.getImageInspect(imageInfo.engine, imageInfo.Id);
|
||||
modalImageInspectInfo = imageInspectInfo;
|
||||
modalExposedPorts = Array.from(Object.keys(modalImageInspectInfo?.Config?.ExposedPorts || {}));
|
||||
modalContainerPortMapping = new Array<string>(modalExposedPorts.length);
|
||||
runContainerFromImageModal = true;
|
||||
}
|
||||
|
||||
function toggleCreateContainer(): void {
|
||||
runContainerFromImageModal = !runContainerFromImageModal;
|
||||
}
|
||||
|
||||
async function startContainer() {
|
||||
console.log('start container', modalContainerPortMapping);
|
||||
console.log('start container', modalContainerName);
|
||||
|
||||
// create ExposedPorts objects
|
||||
const ExposedPorts = {};
|
||||
|
||||
const PortBindings = {};
|
||||
modalExposedPorts.forEach((port, index) => {
|
||||
if (modalContainerPortMapping[index]) {
|
||||
PortBindings[port] = [{HostPort: modalContainerPortMapping[index]}];
|
||||
}
|
||||
ExposedPorts[port] = {};
|
||||
});
|
||||
|
||||
const Image = modalImageInfo.Id;
|
||||
|
||||
const HostConfig = {
|
||||
PortBindings,
|
||||
};
|
||||
|
||||
const options: ContainerCreateOptions = {
|
||||
Image,
|
||||
name: modalContainerName,
|
||||
HostConfig,
|
||||
ExposedPorts,
|
||||
|
||||
}
|
||||
console.log('calling create and start with options', options);
|
||||
const response = await window.createAndStartContainer(modalImageInspectInfo.engine, options);
|
||||
runContainerFromImageModal = false;
|
||||
|
||||
// redirect to containers
|
||||
window.location.href = '#/containers';
|
||||
|
||||
}
|
||||
|
||||
function keydownDockerfileChoice(e: KeyboardEvent) {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
runContainerFromImageModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getId(imageInfo: ImageInfo) {
|
||||
let id = imageInfo.Id;
|
||||
if (id.startsWith('sha256:')) {
|
||||
id = id.substring('sha256:'.length);
|
||||
}
|
||||
return id.substring(0, 12);
|
||||
}
|
||||
|
||||
function getTag(imageInfo: ImageInfo) {
|
||||
// get name
|
||||
return imageInfo.RepoTags.map((tag => {
|
||||
return tag.split(':')[1];
|
||||
})).join(',');
|
||||
|
||||
}
|
||||
|
||||
function getSize(imageInfo: ImageInfo) {
|
||||
return imageInfo.Size;
|
||||
}
|
||||
|
||||
function getColorForState(imageInfo: ImageInfo): string {
|
||||
if (imageInfo.Id === 'running') {
|
||||
return 'text-emerald-500'
|
||||
}
|
||||
return 'text-gray-700';
|
||||
}
|
||||
|
||||
function getEngine(containerInfo: ImageInfo): string {
|
||||
return containerInfo.engine;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col" >
|
||||
|
||||
<div class="min-w-full">
|
||||
<div class="flex flex-row">
|
||||
<div class="py-5 px-5 lg:w-[35rem] w-[22rem]">
|
||||
<div class="flex items-center bg-gray-700 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 ml-2 mr-2 " fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input bind:value={searchTerm} type="text" name="containerSearchName" placeholder="Search...."
|
||||
class="w-full py-2 outline-none bg-gray-700">
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex flex-1 justify-end">
|
||||
<div class="py-5 px-5">
|
||||
<button type="button" on:click="{() => toggleCreateContainer()}" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focus:ring-violet-400 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center mr-2 ">
|
||||
<Fa class="h-10 w-8 cursor-pointer rounded-full text-xl text-white" icon={faPlusCircle} />
|
||||
Create container
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<table class="min-w-full divide-y divide-gray-800">
|
||||
<!--<thead class="bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">State</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ports</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Engine</th>
|
||||
</tr>
|
||||
</thead>-->
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-200">
|
||||
{#each $filtered as image}
|
||||
<tr>
|
||||
<td class="px-6 py-2 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="ml-4">
|
||||
<div class="flex flex-row">
|
||||
<div class="text-sm text-gray-200">{getName(image)}</div>
|
||||
<div class="pl-2 text-sm text-violet-400">{getId(image)}</div>
|
||||
</div>
|
||||
<div class="flex flex-row text-xs font-extra-light text-gray-500">
|
||||
<div>{getTag(image)}</div>
|
||||
<div class="px-2 inline-flex text-xs font-extralight rounded-full bg-slate-900 text-slate-400">{getEngine(image)}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-row text-xs font-extra-light text-gray-500">
|
||||
<!-- <div>{getSize(image)}</div>-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-2 whitespace-nowrap">
|
||||
<div class="flex flex-row justify-end">
|
||||
<button title="Run Image" on:click={() => runImage(image)} ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faPlayCircle} /></button>
|
||||
<!-- <button title="Start Container" on:click={() => startContainer(image)} hidden class:block="{container?.State !== 'running'}" ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faPlayCircle} /></button>
|
||||
<button title="Stop Container" on:click={() => stopContainer(image)} hidden class:block="{container?.State === 'running'}" ><Fa class="h-10 w-10 cursor-pointer rounded-full text-3xl text-sky-800" icon={faStopCircle} /></button>
|
||||
<button title="Delete Container"><Fa class="cursor-pointer h-10 w-10 rounded-full text-3xl text-sky-800" icon={faTrash} /></button>
|
||||
-->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full min-w-full flex flex-col" class:hidden="{$filtered.length > 0}">
|
||||
|
||||
<div class="pf-c-empty-state h-full">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
|
||||
<h1 class="pf-c-title pf-m-lg">No image</h1>
|
||||
<div
|
||||
class="pf-c-empty-state__body"
|
||||
>No image</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{#if runContainerFromImageModal}
|
||||
<div class="modal z-50 fixed w-full h-full top-0 left-0 flex items-center justify-center p-8 lg:p-0" tabindex={0} autofocus on:keydown={keydownDockerfileChoice}>
|
||||
<div class="modal-overlay fixed w-full h-full bg-gray-900 opacity-50"></div>
|
||||
|
||||
|
||||
|
||||
<div class="relative px-4 w-full max-w-4xl h-full md:h-auto">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
|
||||
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<div class="flex justify-end p-2">
|
||||
<button on:click='{() => {runContainerFromImageModal = false}}' type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white" data-modal-toggle="authentication-modal">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!--<form class="px-6 pb-4 space-y-6 lg:px-8 sm:pb-6 xl:pb-8">-->
|
||||
<div class="px-6 pb-4 space-y-6 lg:px-8 sm:pb-6 xl:pb-8">
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white">Create Container {getName(modalImageInfo)}</h3>
|
||||
|
||||
<div>
|
||||
<label for="modalContainerName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Container Name</label>
|
||||
<input type="text" bind:value={modalContainerName} name="modalContainerName" id="modalContainerName" placeholder="Enter container name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required>
|
||||
<!-- add a label for each port-->
|
||||
<label for="modalContainerName" class:hidden="{modalExposedPorts.length === 0}" class="pt-6 block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Port Mapping</label>
|
||||
{#each modalExposedPorts as port, index}
|
||||
<input type="text" bind:value={modalContainerPortMapping[index]} placeholder="Enter value for port {port}" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<button on:click="{() => startContainer()}" class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Start Container</button>
|
||||
|
||||
<!-- </form>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
80
packages/renderer/src/lib/ProviderList.svelte
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts">
|
||||
import {providerInfos} from '../stores/providers';
|
||||
import Fa from 'svelte-fa/src/fa.svelte'
|
||||
import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faStopCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { ProviderInfo } from '../../../preload/src/api/provider-info';
|
||||
|
||||
|
||||
let waiting = false;
|
||||
|
||||
async function startProviderLifecycle(provider: ProviderInfo): Promise<void> {
|
||||
waiting = true;
|
||||
await window.startProviderLifecycle(provider.name)
|
||||
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
|
||||
console.log('receive response from the server side: started');
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
async function stopProviderLifecycle(provider: ProviderInfo): Promise<void> {
|
||||
waiting = true;
|
||||
await window.stopProviderLifecycle(provider.name)
|
||||
console.log('receive response from the server side: stopped');
|
||||
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="grid xl:grid-cols-4 md:grid-cols-2 grid-cols-1 gap-4">
|
||||
|
||||
|
||||
{#each $providerInfos as provider}
|
||||
<div class="rounded overflow-hidden shadow-lg m-5 bg-gray-700">
|
||||
<div class="px-6 py-4">
|
||||
<div class="font-bold text-gray-200 text-sm mb-2 h-10">{provider.name}</div>
|
||||
<div class="text-gray-300 text-sm h-5">
|
||||
{#if provider.lifecycle}
|
||||
Status: {provider.lifecycle.status}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 pt-4 pb-2 h-12">
|
||||
<div class="flex flex-row">
|
||||
{#if provider.lifecycle}
|
||||
|
||||
<button type="button" on:click="{() => startProviderLifecycle(provider)}" class:hidden="{provider?.lifecycle?.status === 'started'}" class="text-white bg-violet-700 hover:bg-violet-800 disabled:hover:bg-gray-600 disabled:bg-gray-500 focus:ring-4 focus:ring-violet-400 font-medium rounded-lg text-xs px-3 py-1.5 text-center inline-flex items-center mr-2 ">
|
||||
{#if waiting === true}
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{:else}
|
||||
<Fa class="h-10 w-8 cursor-pointer rounded-full text-xl text-white" icon={faPlayCircle} />
|
||||
{/if}
|
||||
Start
|
||||
</button>
|
||||
<button type="button" on:click="{() => stopProviderLifecycle(provider)}" class:hidden="{provider?.lifecycle?.status !== 'started'}" class="text-white bg-violet-700 hover:bg-violet-800 disabled:hover:bg-gray-600 disabled:bg-gray-500 focus:ring-4 focus:ring-violet-400 font-medium rounded-lg text-xs px-3 py-1.5 text-center inline-flex items-center">
|
||||
{#if waiting === true}
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{:else}
|
||||
<Fa class="h-10 w-8 cursor-pointer rounded-full text-xl text-white" icon={faStopCircle} />
|
||||
{/if}
|
||||
Stop
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-6 flex-wrap font-thin break-words text-gray-400 text-xs">
|
||||
{#if provider.connection}
|
||||
Connection:
|
||||
{provider.connection}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
8
packages/renderer/src/main.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import App from './App.svelte'
|
||||
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
})
|
||||
|
||||
export default app
|
||||
16
packages/renderer/src/override.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
:root {
|
||||
--pf-global--primary-color--100: #6d28d9;
|
||||
--pf-global--primary-color--light-100: #6d28d9;
|
||||
--pf-global--primary-color--dark-100: #6d28d9;
|
||||
|
||||
--pf-global--primary-color--200: #5b21b6;
|
||||
--pf-global--primary-color--light-200: #5b21b6;
|
||||
--pf-global--primary-color--dark-200: #5b21b6;
|
||||
|
||||
--pf-global--primary-color--300: #6d28d9;
|
||||
--pf-global--primary-color--light-300: #6d28d9;
|
||||
--pf-global--primary-color--dark-300: #6d28d9;
|
||||
|
||||
|
||||
}
|
||||
59
packages/renderer/src/stores/containers.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import type { ContainerInfo } from '../../../preload/src/api/container-info';
|
||||
|
||||
export async function fetchContainers() {
|
||||
const result = await window.listContainers();
|
||||
containersInfos.set(result);
|
||||
}
|
||||
|
||||
fetchContainers();
|
||||
export const containersInfos = writable([]);
|
||||
|
||||
export const searchPattern = writable('');
|
||||
|
||||
function getName(containerInfo: ContainerInfo) {
|
||||
return JSON.stringify(containerInfo).toLowerCase();
|
||||
}
|
||||
|
||||
export const filtered = derived(
|
||||
[searchPattern, containersInfos],
|
||||
([$searchPattern, $containersInfos]) => $containersInfos.filter(containerInfo => getName(containerInfo).includes($searchPattern.toLowerCase())),
|
||||
);
|
||||
|
||||
// need to refresh when extension is started or stopped
|
||||
window.addEventListener('extension-started', () => {
|
||||
fetchContainers();
|
||||
});
|
||||
window.addEventListener('extension-stopped', () => {
|
||||
fetchContainers();
|
||||
});
|
||||
|
||||
window.events?.receive('container-stopped-event', () => {
|
||||
fetchContainers();
|
||||
});
|
||||
|
||||
window.events?.receive('container-started-event', () => {
|
||||
fetchContainers();
|
||||
});
|
||||
|
||||
window.events?.receive('provider-change', () => {
|
||||
fetchContainers();
|
||||
});
|
||||
39
packages/renderer/src/stores/extensions.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export async function fetchExtensions() {
|
||||
const result = await window.listExtensions();
|
||||
extensionInfos.set(result);
|
||||
}
|
||||
|
||||
fetchExtensions();
|
||||
export const extensionInfos = writable([]);
|
||||
|
||||
// need to refresh when extension is started or stopped
|
||||
window.addEventListener('extension-started', () => {
|
||||
fetchExtensions();
|
||||
});
|
||||
window.addEventListener('extension-stopped', () => {
|
||||
fetchExtensions();
|
||||
});
|
||||
|
||||
window?.events.receive('extension-started', () => {
|
||||
fetchExtensions();
|
||||
});
|
||||
55
packages/renderer/src/stores/images.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import type { ImageInfo } from '../../../preload/src/api/image-info';
|
||||
export async function fetchImages() {
|
||||
const result = await window.listImages();
|
||||
imagesInfos.set(result);
|
||||
}
|
||||
|
||||
fetchImages();
|
||||
export const imagesInfos = writable([]);
|
||||
|
||||
export const searchPattern = writable('');
|
||||
|
||||
function getName(imageInfo: ImageInfo) {
|
||||
return JSON.stringify(imageInfo).toLowerCase();
|
||||
}
|
||||
|
||||
export const filtered = derived(
|
||||
[searchPattern, imagesInfos],
|
||||
([$searchPattern, $imagesInfos]) => $imagesInfos.filter(imageInfo => getName(imageInfo).includes($searchPattern.toLowerCase())),
|
||||
);
|
||||
|
||||
// need to refresh when extension is started or stopped
|
||||
window.addEventListener('extension-started', () => {
|
||||
fetchImages();
|
||||
});
|
||||
window.addEventListener('extension-stopped', () => {
|
||||
fetchImages();
|
||||
});
|
||||
|
||||
window.addEventListener('image-build', () => {
|
||||
fetchImages();
|
||||
});
|
||||
|
||||
window?.events.receive('provider-change', () => {
|
||||
console.log('receive provider change event, fetchImages...');
|
||||
fetchImages();
|
||||
});
|
||||
47
packages/renderer/src/stores/providers.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { writable } from 'svelte/store';
|
||||
export async function fetchProviders() {
|
||||
const result = await window.getProviderInfos();
|
||||
console.log('providers result', result);
|
||||
providerInfos.set(result);
|
||||
}
|
||||
|
||||
fetchProviders();
|
||||
export const providerInfos = writable([]);
|
||||
|
||||
// need to refresh when extension is started or stopped
|
||||
window.addEventListener('extension-started', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
window.addEventListener('extension-stopped', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
|
||||
window.addEventListener('provider-lifecycle-change', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
|
||||
window?.events.receive('provider-lifecycle-change', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
|
||||
window?.events.receive('provider-change', () => {
|
||||
fetchProviders();
|
||||
});
|
||||
2
packages/renderer/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
25
packages/renderer/svelte.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
|
||||
export default {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: sveltePreprocess(),
|
||||
};
|
||||
22
packages/renderer/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte",
|
||||
"types/**/*.d.ts",
|
||||
"../../types/**/*.d.ts",
|
||||
"../preload/exposedInMainWorld.d.ts"
|
||||
]
|
||||
}
|
||||
54
packages/renderer/vite.config.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
/* eslint-env node */
|
||||
import {join} from 'path';
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
const PACKAGE_ROOT = __dirname;
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
mode: process.env.MODE,
|
||||
root: PACKAGE_ROOT,
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/': join(PACKAGE_ROOT, 'src') + '/',
|
||||
},
|
||||
},
|
||||
plugins: [svelte()],
|
||||
optimizeDeps: {
|
||||
exclude: ['tinro']
|
||||
},
|
||||
base: '',
|
||||
server: {
|
||||
fs: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
|
||||
emptyOutDir: true,
|
||||
brotliSize: false,
|
||||
},
|
||||
})
|
||||
25
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'postcss-import': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
45
scripts/update-electron-vendors.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const {writeFile} = require('fs/promises');
|
||||
const {execSync} = require('child_process');
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Returns versions of electron vendors
|
||||
* The performance of this feature is very poor and can be improved
|
||||
* @see https://github.com/electron/electron/issues/28006
|
||||
*
|
||||
* @returns {NodeJS.ProcessVersions}
|
||||
*/
|
||||
function getVendors() {
|
||||
const output = execSync(`${electron} -p "JSON.stringify(process.versions)"`, {
|
||||
env: {'ELECTRON_RUN_AS_NODE': '1'},
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
return JSON.parse(output);
|
||||
}
|
||||
|
||||
function updateVendors() {
|
||||
const electronRelease = getVendors();
|
||||
|
||||
const nodeMajorVersion = electronRelease.node.split('.')[0];
|
||||
const chromeMajorVersion = electronRelease.v8.split('.')[0] + electronRelease.v8.split('.')[1];
|
||||
|
||||
const browserslistrcPath = path.resolve(process.cwd(), '.browserslistrc');
|
||||
|
||||
return Promise.all([
|
||||
writeFile('./.electron-vendors.cache.json',
|
||||
JSON.stringify({
|
||||
chrome: chromeMajorVersion,
|
||||
node: nodeMajorVersion,
|
||||
}, null, 2) + '\n',
|
||||
),
|
||||
|
||||
writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, 'utf8'),
|
||||
]);
|
||||
}
|
||||
|
||||
updateVendors().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
158
scripts/watch.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {createServer, build, createLogger} = require('vite');
|
||||
const electronPath = require('electron');
|
||||
const {spawn} = require('child_process');
|
||||
const {generateAsync} = require('dts-for-context-bridge');
|
||||
const path = require('path');
|
||||
|
||||
/** @type 'production' | 'development'' */
|
||||
const mode = process.env.MODE = process.env.MODE || 'development';
|
||||
|
||||
|
||||
/** @type {import('vite').LogLevel} */
|
||||
const LOG_LEVEL = 'info';
|
||||
|
||||
|
||||
/** @type {import('vite').InlineConfig} */
|
||||
const sharedConfig = {
|
||||
mode,
|
||||
build: {
|
||||
watch: {},
|
||||
},
|
||||
logLevel: LOG_LEVEL,
|
||||
};
|
||||
|
||||
/** Messages on stderr that match any of the contained patterns will be stripped from output */
|
||||
const stderrFilterPatterns = [
|
||||
// warning about devtools extension
|
||||
// https://github.com/cawa-93/vite-electron-builder/issues/492
|
||||
// https://github.com/MarshallOfSound/electron-devtools-installer/issues/143
|
||||
/ExtensionLoadWarning/,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {{name: string; configFile: string; writeBundle: import('rollup').OutputPlugin['writeBundle'] }} param0
|
||||
*/
|
||||
const getWatcher = ({name, configFile, writeBundle}) => {
|
||||
return build({
|
||||
...sharedConfig,
|
||||
configFile,
|
||||
plugins: [{name, writeBundle}],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Start or restart App when source files are changed
|
||||
* @param {{config: {server: import('vite').ResolvedServerOptions}}} ResolvedServerOptions
|
||||
*/
|
||||
const setupMainPackageWatcher = ({config: {server}}) => {
|
||||
// Create VITE_DEV_SERVER_URL environment variable to pass it to the main process.
|
||||
{
|
||||
const protocol = server.https ? 'https:' : 'http:';
|
||||
const host = server.host || 'localhost';
|
||||
const port = server.port; // Vite searches for and occupies the first free port: 3000, 3001, 3002 and so on
|
||||
const path = '/';
|
||||
process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`;
|
||||
}
|
||||
|
||||
const logger = createLogger(LOG_LEVEL, {
|
||||
prefix: '[main]',
|
||||
});
|
||||
|
||||
/** @type {ChildProcessWithoutNullStreams | null} */
|
||||
let spawnProcess = null;
|
||||
|
||||
return getWatcher({
|
||||
name: 'reload-app-on-main-package-change',
|
||||
configFile: 'packages/main/vite.config.js',
|
||||
writeBundle() {
|
||||
if (spawnProcess !== null) {
|
||||
spawnProcess.off('exit', process.exit);
|
||||
spawnProcess.kill('SIGINT');
|
||||
spawnProcess = null;
|
||||
}
|
||||
|
||||
spawnProcess = spawn(String(electronPath), ['.']);
|
||||
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
|
||||
spawnProcess.stderr.on('data', d => {
|
||||
const data = d.toString().trim();
|
||||
if (!data) return;
|
||||
const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
|
||||
if (mayIgnore) return;
|
||||
logger.error(data, { timestamp: true });
|
||||
});
|
||||
|
||||
// Stops the watch script when the application has been quit
|
||||
spawnProcess.on('exit', process.exit);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Start or restart App when source files are changed
|
||||
* @param {{ws: import('vite').WebSocketServer}} WebSocketServer
|
||||
*/
|
||||
const setupPreloadPackageWatcher = ({ws}) =>
|
||||
getWatcher({
|
||||
name: 'reload-page-on-preload-package-change',
|
||||
configFile: 'packages/preload/vite.config.js',
|
||||
writeBundle() {
|
||||
// Generating exposedInMainWorld.d.ts when preload package is changed.
|
||||
generateAsync({
|
||||
input: 'packages/preload/tsconfig.json',
|
||||
output: 'packages/preload/exposedInMainWorld.d.ts',
|
||||
});
|
||||
|
||||
ws.send({
|
||||
type: 'full-reload',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Start or restart App when source files are changed
|
||||
* @param {{ws: import('vite').WebSocketServer}} WebSocketServer
|
||||
*/
|
||||
const setupExtensionApiWatcher = (name) =>{
|
||||
let spawnProcess;
|
||||
const folderName = path.resolve(__dirname, '../extensions/' + name);
|
||||
|
||||
console.log('dirname is', folderName);
|
||||
spawnProcess = spawn('yarn', ['--cwd', folderName, 'watch'] );
|
||||
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && console.warn(d.toString(), {timestamp: true}));
|
||||
spawnProcess.stderr.on('data', d => {
|
||||
const data = d.toString().trim();
|
||||
if (!data) return;
|
||||
console.error(data, { timestamp: true });
|
||||
});
|
||||
|
||||
// Stops the watch script when the application has been quit
|
||||
spawnProcess.on('exit', process.exit);
|
||||
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const viteDevServer = await createServer({
|
||||
...sharedConfig,
|
||||
configFile: 'packages/renderer/vite.config.js',
|
||||
});
|
||||
|
||||
await viteDevServer.listen();
|
||||
await setupExtensionApiWatcher('crc');
|
||||
await setupExtensionApiWatcher('docker');
|
||||
await setupExtensionApiWatcher('lima');
|
||||
await setupExtensionApiWatcher('podman');
|
||||
await setupPreloadPackageWatcher(viteDevServer);
|
||||
await setupMainPackageWatcher(viteDevServer);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
32
tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'packages/renderer/index.html',
|
||||
'packages/renderer/src/**/*.{svelte,css}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
transitionProperty: {
|
||||
width: 'width',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
70
tests/e2e.spec.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type {ElectronApplication} from 'playwright';
|
||||
import {_electron as electron} from 'playwright';
|
||||
import {afterAll, beforeAll, expect, test} from 'vitest';
|
||||
import {createHash} from 'crypto';
|
||||
import '../packages/preload/exposedInMainWorld.d.ts';
|
||||
|
||||
let electronApp: ElectronApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
electronApp = await electron.launch({args: ['.']});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
test('Main window state', async () => {
|
||||
const windowState: { isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean }
|
||||
= await electronApp.evaluate(({BrowserWindow}) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
const getState = () => ({
|
||||
isVisible: mainWindow.isVisible(),
|
||||
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
|
||||
isCrashed: mainWindow.webContents.isCrashed(),
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (mainWindow.isVisible()) {
|
||||
resolve(getState());
|
||||
} else
|
||||
mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
|
||||
});
|
||||
});
|
||||
|
||||
expect(windowState.isCrashed, 'App was crashed').toBeFalsy();
|
||||
expect(windowState.isVisible, 'Main window was not visible').toBeTruthy();
|
||||
expect(windowState.isDevToolsOpened, 'DevTools was opened').toBeFalsy();
|
||||
});
|
||||
|
||||
test('Main window web content', async () => {
|
||||
const page = await electronApp.firstWindow();
|
||||
const element = await page.$('#app', {strict: true});
|
||||
expect(element, 'Can\'t find root element').toBeDefined();
|
||||
expect((await element.innerHTML()).trim(), 'Window content was empty').not.equal('');
|
||||
});
|
||||
|
||||
|
||||
test('Preload versions', async () => {
|
||||
const page = await electronApp.firstWindow();
|
||||
const exposedVersions = await page.evaluate(() => globalThis.versions);
|
||||
const expectedVersions = await electronApp.evaluate(() => process.versions);
|
||||
expect(exposedVersions).toBeDefined();
|
||||
expect(exposedVersions).to.deep.equal(expectedVersions);
|
||||
});
|
||||
|
||||
test('Preload nodeCrypto', async () => {
|
||||
const page = await electronApp.firstWindow();
|
||||
|
||||
const exposedNodeCrypto = await page.evaluate(() => globalThis.nodeCrypto);
|
||||
expect(exposedNodeCrypto).toHaveProperty('sha256sum');
|
||||
|
||||
const sha256sumType = await page.evaluate(() => typeof globalThis.nodeCrypto.sha256sum);
|
||||
expect(sha256sumType).toEqual('function');
|
||||
|
||||
const rawTestData = 'raw data';
|
||||
const hash = await page.evaluate((d: string) => globalThis.nodeCrypto.sha256sum(d), rawTestData);
|
||||
const expectedHash = createHash('sha256').update(rawTestData).digest('hex');
|
||||
expect(hash).toEqual(expectedHash);
|
||||
});
|
||||
22
types/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
/**
|
||||
* Describes all existing environment variables and their types.
|
||||
* Required for Code completion and type checking
|
||||
*
|
||||
* Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code
|
||||
*
|
||||
* @see https://github.com/vitejs/vite/blob/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface
|
||||
* @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc
|
||||
*/
|
||||
interface ImportMetaEnv {
|
||||
|
||||
/**
|
||||
* The value of the variable is set in scripts/watch.js and depend on packages/main/vite.config.js
|
||||
*/
|
||||
readonly VITE_DEV_SERVER_URL: undefined | string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
41
vitest.config.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022 Red Hat, Inc.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
/**
|
||||
* Config for global end-to-end tests
|
||||
* placed in project root tests folder
|
||||
* @type {import('vite').UserConfig}
|
||||
* @see https://vitest.dev/config/
|
||||
*/
|
||||
const config = {
|
||||
test: {
|
||||
/**
|
||||
* By default, vitest search test files in all packages.
|
||||
* For e2e tests have sense search only is project root tests folder
|
||||
*/
|
||||
include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
|
||||
/**
|
||||
* A default timeout of 5000ms is sometimes not enough for playwright.
|
||||
*/
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||