feat: initial import

This commit is contained in:
Florent Benoit 2022-03-08 17:49:08 +01:00
parent bb68b29a09
commit 465bb8a493
91 changed files with 9378 additions and 0 deletions

1
.browserslistrc Normal file
View file

@ -0,0 +1 @@
Chrome 98

18
.editorconfig Normal file
View 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

View 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;

View file

@ -0,0 +1,4 @@
{
"chrome": "98",
"node": "16"
}

54
.eslintrc.json Normal file
View 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
View file

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
.eslintcache
*.cdix

202
LICENSE Normal file
View file

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

0
README.md Normal file
View file

0
buildResources/.gitkeep Normal file
View file

BIN
buildResources/icon.icns Normal file

Binary file not shown.

BIN
buildResources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

59
buildResources/icon.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View 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
View 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);
});

View 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');
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View 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"
}
}

View 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);
});
*/

View 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');
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View 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"
}
}

View 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);
});
*/

View 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');
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View 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"
}
}

View 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);
});

View 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');
}

View 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
View 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"
}
}

View 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": {}
}

View 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>;
}
}

View 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"]
}

View 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
View 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)
})

View 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();
}

View 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;
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

View 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

View 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);
});

View 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"
]
}

View 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;

View 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;
}

View 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[]>;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}
}

View 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,
};
});
}
}

View 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);
}
}
}

View 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();

View 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);
}
}

View 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"
]
}

View 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;

View file

@ -0,0 +1,13 @@
{
"env": {
"browser": true,
"node": false
},
"extends": [
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 12,
"sourceType": "module"
}
}

View 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

View 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>

View 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
View file

@ -0,0 +1,4 @@
/node_modules/
/dist/
/.vscode/
.DS_Store

View 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>
-->

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View 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');
},
},
];
}
}

View 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>

View 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}

View 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>

View 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}

View 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>

View file

@ -0,0 +1,8 @@
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')
})
export default app

View 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;
}

View 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();
});

View 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();
});

View 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();
});

View 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
View file

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View 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(),
};

View 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"
]
}

View 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
View 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: {},
},
};

View 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
View 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
View 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
View 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
View 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
View 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;

4251
yarn.lock Normal file

File diff suppressed because it is too large Load diff