mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
feat: Adds support of "podman play kube" through REST API
fixes https://github.com/containers/podman-desktop/issues/203 Change-Id: Id68146bb7f9d871765de7e130eaacde6de1d0834 Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
parent
8ad6777380
commit
e25b846f39
11 changed files with 327 additions and 32 deletions
|
|
@ -16,7 +16,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type { BrowserWindowConstructorOptions } from 'electron';
|
||||
import type { BrowserWindowConstructorOptions, FileFilter } from 'electron';
|
||||
import { BrowserWindow, ipcMain, app, dialog, screen } from 'electron';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
import { join } from 'path';
|
||||
|
|
@ -80,9 +80,10 @@ async function createWindow() {
|
|||
});
|
||||
|
||||
// select a file using native widget
|
||||
ipcMain.on('dialog:openFile', async (_, param: { dialogId: string; message: string }) => {
|
||||
ipcMain.on('dialog:openFile', async (_, param: { dialogId: string; message: string; filter: FileFilter }) => {
|
||||
const response = await dialog.showOpenDialog(browserWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [param.filter],
|
||||
message: param.message,
|
||||
});
|
||||
// send the response back
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface ProviderContainerConnectionInfo {
|
|||
socketPath: string;
|
||||
};
|
||||
lifecycleMethods?: LifecycleMethod[];
|
||||
type: 'docker' | 'podman';
|
||||
}
|
||||
|
||||
export interface ProviderKubernetesConnectionInfo {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const tar: { pack: (dir: string) => NodeJS.ReadableStream } = require('tar-fs');
|
|||
import { EventEmitter } from 'node:events';
|
||||
import type { ContainerInspectInfo } from './api/container-inspect-info';
|
||||
import type { HistoryInfo } from './api/history-info';
|
||||
import type { LibPod, PodInfo as LibpodPodInfo } from './dockerode/libpod-dockerode';
|
||||
import type { LibPod, PlayKubeInfo, PodInfo as LibpodPodInfo } from './dockerode/libpod-dockerode';
|
||||
import { LibpodDockerode } from './dockerode/libpod-dockerode';
|
||||
import type { ContainerStatsInfo } from './api/container-stats-info';
|
||||
import type { VolumeInfo, VolumeInspectInfo, VolumeListInfo } from './api/volume-info';
|
||||
|
|
@ -788,6 +788,21 @@ export class ContainerProviderRegistry {
|
|||
return this.statsConsumerId;
|
||||
}
|
||||
|
||||
async playKube(
|
||||
kubernetesYamlFilePath: string,
|
||||
selectedProvider: ProviderContainerConnectionInfo,
|
||||
): Promise<PlayKubeInfo> {
|
||||
this.telemetryService.track('playKube');
|
||||
// grab all connections
|
||||
const matchingContainerProvider = Array.from(this.internalProviders.values()).find(
|
||||
containerProvider => containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath,
|
||||
);
|
||||
if (!matchingContainerProvider || !matchingContainerProvider.libpodApi) {
|
||||
throw new Error('No provider with a running engine');
|
||||
}
|
||||
return matchingContainerProvider.libpodApi.playKube(kubernetesYamlFilePath);
|
||||
}
|
||||
|
||||
async buildImage(
|
||||
containerBuildContextDirectory: string,
|
||||
relativeContainerfilePath: string,
|
||||
|
|
@ -798,7 +813,9 @@ export class ContainerProviderRegistry {
|
|||
this.telemetryService.track('buildImage');
|
||||
// grab all connections
|
||||
const matchingContainerProvider = Array.from(this.internalProviders.values()).find(
|
||||
containerProvider => containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath,
|
||||
containerProvider =>
|
||||
containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath &&
|
||||
selectedProvider.status === 'started',
|
||||
);
|
||||
if (!matchingContainerProvider || !matchingContainerProvider.api) {
|
||||
throw new Error('No provider with a running engine');
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ export interface PodInfo {
|
|||
Status: string;
|
||||
}
|
||||
|
||||
export interface PlayKubePodInfo {
|
||||
ContainerErrors: string[];
|
||||
Containers: string[];
|
||||
Id: string;
|
||||
InitContainers: string[];
|
||||
Logs: string[];
|
||||
}
|
||||
|
||||
export interface PlayKubeInfo {
|
||||
Pods: PlayKubePodInfo[];
|
||||
RmReport: { Err: string; Id: string }[];
|
||||
Secrets: { CreateReport: { ID: string } }[];
|
||||
StopReport: { Err: string; Id: string }[];
|
||||
Volumes: { Name: string }[];
|
||||
}
|
||||
|
||||
// API of libpod that we want to expose on our side
|
||||
export interface LibPod {
|
||||
listPods(): Promise<PodInfo[]>;
|
||||
|
|
@ -45,6 +61,7 @@ export interface LibPod {
|
|||
removePod(podId: string): Promise<void>;
|
||||
restartPod(podId: string): Promise<void>;
|
||||
generateKube(names: string[]): Promise<string>;
|
||||
playKube(yamlContentFilePath: string): Promise<PlayKubeInfo>;
|
||||
}
|
||||
|
||||
// tweak Dockerode by adding the support of libpod API
|
||||
|
|
@ -209,5 +226,29 @@ export class LibpodDockerode {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
// add playKube
|
||||
prototypeOfDockerode.playKube = function (yamlContentFilePath: string) {
|
||||
const optsf = {
|
||||
path: '/v4.2.0/libpod/play/kube',
|
||||
method: 'POST',
|
||||
file: yamlContentFilePath,
|
||||
statusCodes: {
|
||||
200: true,
|
||||
204: true,
|
||||
500: 'server error',
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.modem.dial(optsf, (err: unknown, data: unknown) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import type { HistoryInfo } from './api/history-info';
|
|||
import type { PodInfo } from './api/pod-info';
|
||||
import type { VolumeInspectInfo, VolumeListInfo } from './api/volume-info';
|
||||
import type { ContainerStatsInfo } from './api/container-stats-info';
|
||||
import type { PlayKubeInfo } from './dockerode/libpod-dockerode';
|
||||
|
||||
type LogType = 'log' | 'warn' | 'trace' | 'debug' | 'error';
|
||||
export class PluginSystem {
|
||||
|
|
@ -281,6 +282,17 @@ export class PluginSystem {
|
|||
return containerProviderRegistry.generatePodmanKube(engine, names);
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcHandle(
|
||||
'container-provider-registry:playKube',
|
||||
async (
|
||||
_listener,
|
||||
yamlFilePath: string,
|
||||
selectedProvider: ProviderContainerConnectionInfo,
|
||||
): Promise<PlayKubeInfo> => {
|
||||
return containerProviderRegistry.playKube(yamlFilePath, selectedProvider);
|
||||
},
|
||||
);
|
||||
this.ipcHandle(
|
||||
'container-provider-registry:startContainer',
|
||||
async (_listener, engine: string, containerId: string): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ export class ProviderRegistry {
|
|||
const containerProviderConnection: ProviderContainerConnectionInfo = {
|
||||
name: connection.name,
|
||||
status: connection.status(),
|
||||
type: connection.type,
|
||||
endpoint: {
|
||||
socketPath: connection.endpoint.socketPath,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import type { IConfigurationPropertyRecordedSchema } from '../../main/src/plugin
|
|||
import type { PullEvent } from '../../main/src/plugin/api/pull-event';
|
||||
import { Deferred } from './util/deferred';
|
||||
import type { StatusBarEntryDescriptor } from '../../main/src/plugin/statusbar/statusbar-registry';
|
||||
import type { PlayKubeInfo } from '../../main/src/plugin/dockerode/libpod-dockerode';
|
||||
|
||||
export type DialogResultCallback = (openDialogReturnValue: Electron.OpenDialogReturnValue) => void;
|
||||
|
||||
|
|
@ -142,6 +143,17 @@ function initExposure(): void {
|
|||
contextBridge.exposeInMainWorld('generatePodmanKube', async (engine: string, names: string[]): Promise<string> => {
|
||||
return ipcInvoke('container-provider-registry:generatePodmanKube', engine, names);
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
'playKube',
|
||||
async (
|
||||
relativeContainerfilePath: string,
|
||||
selectedProvider: ProviderContainerConnectionInfo,
|
||||
): Promise<PlayKubeInfo> => {
|
||||
return ipcInvoke('container-provider-registry:playKube', relativeContainerfilePath, selectedProvider);
|
||||
},
|
||||
);
|
||||
|
||||
contextBridge.exposeInMainWorld('stopPod', async (engine: string, podId: string): Promise<void> => {
|
||||
return ipcInvoke('container-provider-registry:stopPod', engine, podId);
|
||||
});
|
||||
|
|
@ -650,28 +662,32 @@ function initExposure(): void {
|
|||
|
||||
const dialogResponses = new Map<string, DialogResultCallback>();
|
||||
|
||||
contextBridge.exposeInMainWorld('openFileDialog', async (message: string) => {
|
||||
// generate id
|
||||
const dialogId = idDialog;
|
||||
idDialog++;
|
||||
contextBridge.exposeInMainWorld(
|
||||
'openFileDialog',
|
||||
async (message: string, filter?: { extensions: string[]; name: string }) => {
|
||||
// generate id
|
||||
const dialogId = idDialog;
|
||||
idDialog++;
|
||||
|
||||
// create defer object
|
||||
const defer = new Deferred<Electron.OpenDialogReturnValue>();
|
||||
// create defer object
|
||||
const defer = new Deferred<Electron.OpenDialogReturnValue>();
|
||||
|
||||
// store the dialogID
|
||||
dialogResponses.set(`${dialogId}`, (result: Electron.OpenDialogReturnValue) => {
|
||||
defer.resolve(result);
|
||||
});
|
||||
// store the dialogID
|
||||
dialogResponses.set(`${dialogId}`, (result: Electron.OpenDialogReturnValue) => {
|
||||
defer.resolve(result);
|
||||
});
|
||||
|
||||
// ask to open file dialog
|
||||
ipcRenderer.send('dialog:openFile', {
|
||||
dialogId: `${dialogId}`,
|
||||
message,
|
||||
});
|
||||
// ask to open file dialog
|
||||
ipcRenderer.send('dialog:openFile', {
|
||||
dialogId: `${dialogId}`,
|
||||
message,
|
||||
filter,
|
||||
});
|
||||
|
||||
// wait for response
|
||||
return defer.promise;
|
||||
});
|
||||
// wait for response
|
||||
return defer.promise;
|
||||
},
|
||||
);
|
||||
|
||||
contextBridge.exposeInMainWorld('openFolderDialog', async (message: string) => {
|
||||
// generate id
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import ImageDetails from './lib/image/ImageDetails.svelte';
|
|||
import PodsList from './lib/pod/PodsList.svelte';
|
||||
import VolumesList from './lib/volume/VolumesList.svelte';
|
||||
import VolumeDetails from './lib/volume/VolumeDetails.svelte';
|
||||
import ContainerPlayKubefile from './lib/container/ContainerPlayKubefile.svelte';
|
||||
let containersCountValue;
|
||||
|
||||
router.mode.hash();
|
||||
|
|
@ -378,6 +379,10 @@ window.events?.receive('display-help', () => {
|
|||
<Route path="/containers/:id/*" let:meta>
|
||||
<ContainerDetails containerID="{meta.params.id}" />
|
||||
</Route>
|
||||
<Route path="/containers/play">
|
||||
<ContainerPlayKubefile />
|
||||
</Route>
|
||||
|
||||
<Route path="/images">
|
||||
<ImagesList />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import NavPage from './ui/NavPage.svelte';
|
|||
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import ContainerGroupIcon from './container/ContainerGroupIcon.svelte';
|
||||
import KubePlayIcon from './container/KubePlayIcon.svelte';
|
||||
|
||||
const containerUtils = new ContainerUtils();
|
||||
let openChoiceModal = false;
|
||||
|
|
@ -39,6 +40,13 @@ $: providerConnections = $providerInfos
|
|||
.flat()
|
||||
.filter(providerContainerConnection => providerContainerConnection.status === 'started');
|
||||
|
||||
$: providerPodmanConnections = $providerInfos
|
||||
.map(provider => provider.containerConnections)
|
||||
.flat()
|
||||
// keep only podman providers as it is not supported by docker
|
||||
.filter(providerContainerConnection => providerContainerConnection.type === 'podman')
|
||||
.filter(providerContainerConnection => providerContainerConnection.status === 'started');
|
||||
|
||||
// number of selected items in the list
|
||||
$: selectedItemsNumber =
|
||||
containerGroups.reduce(
|
||||
|
|
@ -212,6 +220,10 @@ function toggleCreateContainer(): void {
|
|||
openChoiceModal = !openChoiceModal;
|
||||
}
|
||||
|
||||
function runContainerYaml(): void {
|
||||
router.goto('/containers/play');
|
||||
}
|
||||
|
||||
function fromDockerfile(): void {
|
||||
openChoiceModal = false;
|
||||
router.goto('/images/build');
|
||||
|
|
@ -237,16 +249,26 @@ function toggleAllContainerGroups(value: boolean) {
|
|||
bind:searchTerm
|
||||
title="containers"
|
||||
subtitle="Hover over a container to view action buttons; click to open up full details.">
|
||||
<button
|
||||
slot="additional-actions"
|
||||
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 slot="additional-actions" class="space-x-2 flex flex-nowrap">
|
||||
<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>
|
||||
{#if providerPodmanConnections.length > 0}
|
||||
<button
|
||||
on:click="{() => runContainerYaml()}"
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="button"
|
||||
title="Run pod/containers from kubernetes .YAML file ">
|
||||
<div class="flex flex-row align-text-top justify-start items-center">
|
||||
<KubePlayIcon />
|
||||
Play YAML
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div slot="bottom-additional-actions" class="flex flex-row justify-start items-center w-full">
|
||||
{#if selectedItemsNumber > 0}
|
||||
<button
|
||||
|
|
|
|||
163
packages/renderer/src/lib/container/ContainerPlayKubefile.svelte
Normal file
163
packages/renderer/src/lib/container/ContainerPlayKubefile.svelte
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick, onDestroy } from 'svelte';
|
||||
import type { Unsubscriber } from 'svelte/store';
|
||||
import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
|
||||
let providerUnsubscribe: Unsubscriber;
|
||||
|
||||
import { providerInfos } from '../../stores/providers';
|
||||
import MonacoEditor from '../editor/MonacoEditor.svelte';
|
||||
import NoContainerEngineEmptyScreen from '../image/NoContainerEngineEmptyScreen.svelte';
|
||||
import KubePlayIcon from './KubePlayIcon.svelte';
|
||||
|
||||
let runStarted = false;
|
||||
let runFinished = false;
|
||||
let runError = '';
|
||||
let kubernetesYamlFilePath = undefined;
|
||||
let hasInvalidFields = true;
|
||||
|
||||
let playKubeResultRaw;
|
||||
|
||||
let providers: ProviderInfo[] = [];
|
||||
$: providerConnections = providers
|
||||
.map(provider => provider.containerConnections)
|
||||
.flat()
|
||||
// keep only podman providers as it is not supported by docker
|
||||
.filter(providerContainerConnection => providerContainerConnection.type === 'podman')
|
||||
.filter(providerContainerConnection => providerContainerConnection.status === 'started');
|
||||
$: selectedProviderConnection = providerConnections.length > 0 ? providerConnections[0] : undefined;
|
||||
let selectedProvider: ProviderContainerConnectionInfo = undefined;
|
||||
$: selectedProvider = !selectedProvider && selectedProviderConnection ? selectedProviderConnection : selectedProvider;
|
||||
|
||||
function removeEmptyOrNull(obj: any) {
|
||||
Object.keys(obj).forEach(
|
||||
k =>
|
||||
(obj[k] && typeof obj[k] === 'object' && removeEmptyOrNull(obj[k])) ||
|
||||
(!obj[k] && obj[k] !== undefined && delete obj[k]),
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function playKubeFile(): Promise<void> {
|
||||
runStarted = true;
|
||||
runFinished = false;
|
||||
runError = '';
|
||||
if (kubernetesYamlFilePath) {
|
||||
try {
|
||||
const result = await window.playKube(kubernetesYamlFilePath, selectedProvider);
|
||||
// remove the null values from the result
|
||||
playKubeResultRaw = JSON.stringify(removeEmptyOrNull(result), null, 2);
|
||||
runFinished = true;
|
||||
} catch (error) {
|
||||
runError = error;
|
||||
console.error('error playing kube file', error);
|
||||
}
|
||||
}
|
||||
runStarted = false;
|
||||
}
|
||||
function goToContainerList(): void {
|
||||
window.location.href = '#/containers';
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
providerUnsubscribe = providerInfos.subscribe(value => {
|
||||
providers = value;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (providerUnsubscribe) {
|
||||
providerUnsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
async function getKubernetesfileLocation() {
|
||||
const result = await window.openFileDialog('Select .YAML file to play', { name: 'YAML files', extensions: ['yaml'] });
|
||||
if (!result.canceled && result.filePaths.length === 1) {
|
||||
kubernetesYamlFilePath = result.filePaths[0];
|
||||
hasInvalidFields = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if providerConnections.length === 0}
|
||||
<NoContainerEngineEmptyScreen />
|
||||
{/if}
|
||||
|
||||
{#if providerConnections.length > 0}
|
||||
<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-white">Run pod/containers from a Kubernetes .YAML file</h3>
|
||||
<div hidden="{runStarted}">
|
||||
<label for="containerFilePath" class="block mb-2 text-sm font-medium text-gray-300">Kubernetes .YAML file</label>
|
||||
<input
|
||||
on:click="{() => getKubernetesfileLocation()}"
|
||||
name="containerFilePath"
|
||||
id="containerFilePath"
|
||||
bind:value="{kubernetesYamlFilePath}"
|
||||
readonly
|
||||
placeholder="Select .YAML file to run..."
|
||||
class=" text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-600 border-gray-500 placeholder-gray-400 text-white"
|
||||
required />
|
||||
</div>
|
||||
|
||||
<div hidden="{runStarted}">
|
||||
{#if providerConnections.length > 1}
|
||||
<label for="providerConnectionName" class="py-6 block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
>Container Engine
|
||||
<select
|
||||
class="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-600 border-gray-500 placeholder-gray-400 text-white"
|
||||
name="providerChoice"
|
||||
bind:value="{selectedProvider}">
|
||||
{#each providerConnections as providerConnection}
|
||||
<option value="{providerConnection}">{providerConnection.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
{#if providerConnections.length == 1}
|
||||
<input type="hidden" name="providerChoice" readonly bind:value="{selectedProviderConnection.name}" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !runFinished}
|
||||
<button
|
||||
on:click="{() => playKubeFile()}"
|
||||
disabled="{hasInvalidFields || runStarted}"
|
||||
class="w-full pf-c-button pf-m-primary"
|
||||
type="button">
|
||||
<div class="flex flex-row align-text-top justify-center items-center">
|
||||
{#if runStarted}
|
||||
<div class="mr-4">
|
||||
<i class="pf-c-button__progress">
|
||||
<span class="pf-c-spinner pf-m-md" role="progressbar">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
{:else}
|
||||
<KubePlayIcon />
|
||||
{/if}
|
||||
|
||||
Run
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{#if runStarted}
|
||||
<div class="text-gray-400 text-sm">
|
||||
Please wait during the Play Kube and do not change screen. This process may take a few minutes to complete...
|
||||
</div>
|
||||
{/if}
|
||||
{#if runError}
|
||||
<div class="text-red-500 text-sm">{runError}</div>
|
||||
{/if}
|
||||
{#if runFinished}
|
||||
<button on:click="{() => goToContainerList()}" class="w-full pf-c-button pf-m-primary">Done</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if playKubeResultRaw}
|
||||
<div class=" h-full w-full px-6 pb-4 space-y-6 lg:px-8 sm:pb-6 xl:pb-8">
|
||||
<MonacoEditor content="{playKubeResultRaw}" language="json" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
16
packages/renderer/src/lib/container/KubePlayIcon.svelte
Normal file
16
packages/renderer/src/lib/container/KubePlayIcon.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg class="mr-1" width="13px" height="13px" version="1.1" viewBox="0 0 18.035 17.5" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-.99263 -1.1742)">
|
||||
<g
|
||||
transform="matrix(1.0149 0 0 1.0149 16.902 -2.6987)"
|
||||
style="stroke-miterlimit:1;stroke-width:.19707;stroke:#b50000">
|
||||
<path
|
||||
d="m-6.8492 4.2725a1.1191 1.11 0 0 0-0.42888 0.10853l-5.8524 2.7963a1.1191 1.11 0 0 0-0.60552 0.75298l-1.4438 6.2813a1.1191 1.11 0 0 0 0.15194 0.85103 1.1191 1.11 0 0 0 0.06362 0.08832l4.0508 5.0366a1.1191 1.11 0 0 0 0.87498 0.41765l6.4961-0.0015a1.1191 1.11 0 0 0 0.87498-0.41691l4.0493-5.0373a1.1191 1.11 0 0 0 0.21631-0.93935l-1.4461-6.2813a1.1191 1.11 0 0 0-0.60552-0.75298l-5.8532-2.7948a1.1191 1.11 0 0 0-0.54265-0.10853z"
|
||||
style="fill:none;stroke-miterlimit:1;stroke-width:.7;stroke:currentColor"></path>
|
||||
</g>
|
||||
<g transform="matrix(1.7787 0 0 1.7787 -7.5265 -6.6549)" style="fill:currentColor">
|
||||
<path d="m6.2618 7.0361 3.6208-1.05 3.6208 1.05-3.6208 1.05z" style="fill-rule:evenodd;fill:currentColor"></path>
|
||||
<path d="m6.2618 7.4382v3.8528l3.3736 1.8687 0.0167-4.7132z" style="fill-rule:evenodd;fill:currentColor"></path>
|
||||
<path d="m13.503 7.4382v3.8528l-3.3736 1.8687-0.0167-4.7132z" style="fill-rule:evenodd;fill:currentColor"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
Reference in a new issue