mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-08 15:15:02 -04:00
Merge remote-tracking branch 'origin/main'
* origin/main: refactor(qr-code): multiline input refactor(docker-run-converter): improve error handling refactor(docker-run-converter): improved converter refactor(tool): better new tool logic feat(new-tool): simple benchmark calculator feat(new-tool): simple benchmark calculator feat(new-tool): simple benchmark calculator # Conflicts: # src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue
This commit is contained in:
commit
73b1369f50
11 changed files with 243 additions and 17 deletions
|
@ -55,6 +55,7 @@ export const tool = defineTool({
|
||||||
keywords: ['${toolName.split('-').join("', '")}'],
|
keywords: ['${toolName.split('-').join("', '")}'],
|
||||||
component: () => import('./${toolName}.vue'),
|
component: () => import('./${toolName}.vue'),
|
||||||
icon: ArrowsShuffle,
|
icon: ArrowsShuffle,
|
||||||
|
createdAt: new Date('${new Date().toISOString().split('T')[0]}'),
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
34
src/tools/benchmark-builder/benchmark-builder.models.ts
Normal file
34
src/tools/benchmark-builder/benchmark-builder.models.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export { computeAverage, computeVariance, arrayToMarkdownTable };
|
||||||
|
|
||||||
|
function computeAverage({ data }: { data: number[] }) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.sum(data) / data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeVariance({ data }: { data: number[] }) {
|
||||||
|
const mean = computeAverage({ data });
|
||||||
|
|
||||||
|
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
|
||||||
|
|
||||||
|
return computeAverage({ data: squaredDiffs });
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
const rows = data.map((obj) => Object.values(obj));
|
||||||
|
|
||||||
|
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
|
||||||
|
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
||||||
|
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
|
||||||
|
|
||||||
|
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
||||||
|
}
|
109
src/tools/benchmark-builder/benchmark-builder.vue
Normal file
109
src/tools/benchmark-builder/benchmark-builder.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<n-scrollbar style="flex: 1" x-scrollable>
|
||||||
|
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
|
||||||
|
<div v-for="(suite, index) of suites" :key="index">
|
||||||
|
<n-card style="width: 292px; margin: 0 8px 5px">
|
||||||
|
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
|
||||||
|
<n-input v-model:value="suite.title" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-divider></n-divider>
|
||||||
|
<n-form-item label="Suite values" :show-feedback="false">
|
||||||
|
<dynamic-values v-model:values="suite.data" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-space justify="center">
|
||||||
|
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Trash" depth="3" />
|
||||||
|
</template>
|
||||||
|
Delete suite
|
||||||
|
</n-button>
|
||||||
|
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Plus" depth="3" />
|
||||||
|
</template>
|
||||||
|
Add suite
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
<br />
|
||||||
|
</n-scrollbar>
|
||||||
|
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto">
|
||||||
|
<n-table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ header.position }}</th>
|
||||||
|
<th>{{ header.title }}</th>
|
||||||
|
<th>{{ header.size }}</th>
|
||||||
|
<th>{{ header.mean }}</th>
|
||||||
|
<th>{{ header.variance }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
||||||
|
<td>{{ position }}</td>
|
||||||
|
<td>{{ title }}</td>
|
||||||
|
<td>{{ size }}</td>
|
||||||
|
<td>{{ mean }}</td>
|
||||||
|
<td>{{ variance }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
<br />
|
||||||
|
<n-space justify="center">
|
||||||
|
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Trash, Plus } from '@vicons/tabler';
|
||||||
|
import { useClipboard, useStorage } from '@vueuse/core';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
|
||||||
|
import DynamicValues from './dynamic-values.vue';
|
||||||
|
|
||||||
|
const suites = useStorage('benchmark-builder:suites', [
|
||||||
|
{ title: 'Suite 1', data: [5, 10] },
|
||||||
|
{ title: 'Suite 2', data: [8, 12] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const results = computed(() => {
|
||||||
|
return suites.value
|
||||||
|
.map(({ data: dirtyData, title }) => {
|
||||||
|
const data = dirtyData.filter(_.isNumber);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
size: data.length,
|
||||||
|
mean: computeAverage({ data }),
|
||||||
|
variance: computeVariance({ data }),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.mean - b.mean)
|
||||||
|
.map((value, index) => ({ position: index + 1, ...value }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
title: 'Suite name',
|
||||||
|
size: 'Sample count',
|
||||||
|
mean: 'Mean',
|
||||||
|
variance: 'Variance',
|
||||||
|
position: 'Position',
|
||||||
|
};
|
||||||
|
|
||||||
|
function copyAsMarkdown() {
|
||||||
|
copy(arrayToMarkdownTable({ data: results.value, headerMap: header }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
61
src/tools/benchmark-builder/dynamic-values.vue
Normal file
61
src/tools/benchmark-builder/dynamic-values.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5">
|
||||||
|
<n-input-number
|
||||||
|
:ref="refs.set"
|
||||||
|
v-model:value="values[index]"
|
||||||
|
:show-button="false"
|
||||||
|
placeholder="Set your measure..."
|
||||||
|
autofocus
|
||||||
|
@keydown.enter="onInputEnter(index)"
|
||||||
|
/>
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button circle quaternary @click="values.splice(index, 1)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Trash" depth="3" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
Delete value
|
||||||
|
</n-tooltip>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<n-button tertiary @click="addValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Plus" />
|
||||||
|
</template>
|
||||||
|
Add a measure
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Trash, Plus } from '@vicons/tabler';
|
||||||
|
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
||||||
|
import { NInputNumber } from 'naive-ui';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
const refs = useTemplateRefsList<typeof NInputNumber>();
|
||||||
|
|
||||||
|
const props = defineProps<{ values: (number | null)[] }>();
|
||||||
|
const emit = defineEmits(['update:values']);
|
||||||
|
const values = useVModel(props, 'values', emit);
|
||||||
|
|
||||||
|
async function addValue() {
|
||||||
|
values.value.push(null);
|
||||||
|
await nextTick();
|
||||||
|
refs.value.at(-1)?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputEnter(index: number) {
|
||||||
|
if (index === values.value.length - 1) {
|
||||||
|
addValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refs.value.at(index + 1)?.focus();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
11
src/tools/benchmark-builder/index.ts
Normal file
11
src/tools/benchmark-builder/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { SpeedFilled } from '@vicons/material';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'Benchmark builder',
|
||||||
|
path: '/benchmark-builder',
|
||||||
|
description: 'Easily compare execution time of tasks with this very simple online benchmark builder.',
|
||||||
|
keywords: ['benchmark', 'builder', 'execution', 'duration', 'mean', 'variance'],
|
||||||
|
component: () => import('./benchmark-builder.vue'),
|
||||||
|
icon: SpeedFilled,
|
||||||
|
});
|
|
@ -22,7 +22,9 @@
|
||||||
<div v-if="notComposable.length > 0">
|
<div v-if="notComposable.length > 0">
|
||||||
<br />
|
<br />
|
||||||
<n-alert title="This options are not translatable to docker-compose" type="info">
|
<n-alert title="This options are not translatable to docker-compose" type="info">
|
||||||
{{ notComposable }}
|
<ul>
|
||||||
|
<li v-for="(message, index) of notComposable" :key="index">{{ message }}</li>
|
||||||
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -32,14 +34,18 @@
|
||||||
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
||||||
type="warning"
|
type="warning"
|
||||||
>
|
>
|
||||||
{{ notImplemented }}
|
<ul>
|
||||||
|
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
|
||||||
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errors.length > 0">
|
<div v-if="errors.length > 0">
|
||||||
<br />
|
<br />
|
||||||
<n-alert title="The following errors occured" type="error">
|
<n-alert title="The following errors occured" type="error">
|
||||||
{{ errors }}
|
<ul>
|
||||||
|
<li v-for="(message, index) of errors" :key="index">{{ message }}</li>
|
||||||
|
</ul>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,22 +69,15 @@ const conversionResult = computed(() =>
|
||||||
);
|
);
|
||||||
const dockerCompose = computed(() => conversionResult.value.yaml);
|
const dockerCompose = computed(() => conversionResult.value.yaml);
|
||||||
const notImplemented = computed(() =>
|
const notImplemented = computed(() =>
|
||||||
conversionResult.value.messages
|
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
|
||||||
.filter((msg) => msg.type === MessageType.notImplemented)
|
|
||||||
.map((msg) => msg.value)
|
|
||||||
.join('<br>'),
|
|
||||||
);
|
);
|
||||||
const notComposable = computed(() =>
|
const notComposable = computed(() =>
|
||||||
conversionResult.value.messages
|
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
|
||||||
.filter((msg) => msg.type === MessageType.notTranslatable)
|
|
||||||
.map((msg) => msg.value)
|
|
||||||
.join('<br>'),
|
|
||||||
);
|
);
|
||||||
const errors = computed(() =>
|
const errors = computed(() =>
|
||||||
conversionResult.value.messages
|
conversionResult.value.messages
|
||||||
.filter((msg) => msg.type === MessageType.errorDuringConversion)
|
.filter((msg) => msg.type === MessageType.errorDuringConversion)
|
||||||
.map((msg) => msg.value)
|
.map((msg) => msg.value),
|
||||||
.join('<br>'),
|
|
||||||
);
|
);
|
||||||
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
|
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
|
||||||
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||||
|
import { tool as benchmarkBuilder } from './benchmark-builder';
|
||||||
import { tool as ipv4SubnetCalculator } from './ipv4-subnet-calculator';
|
import { tool as ipv4SubnetCalculator } from './ipv4-subnet-calculator';
|
||||||
import { tool as dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter';
|
import { tool as dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter';
|
||||||
import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor';
|
import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor';
|
||||||
|
@ -107,7 +108,7 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Measurement',
|
name: 'Measurement',
|
||||||
components: [chronometer, temperatureConverter],
|
components: [chronometer, temperatureConverter, benchmarkBuilder],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
<n-gi span="2">
|
<n-gi span="2">
|
||||||
<n-form label-width="130" label-placement="left">
|
<n-form label-width="130" label-placement="left">
|
||||||
<n-form-item label="Text:">
|
<n-form-item label="Text:">
|
||||||
<n-input v-model:value="text" placeholder="Your link or text..." />
|
<n-input
|
||||||
|
v-model:value="text"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 1 }"
|
||||||
|
placeholder="Your link or text..."
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="Foreground color:">
|
<n-form-item label="Foreground color:">
|
||||||
<n-color-picker v-model:value="foreground" :modes="['hex']" />
|
<n-color-picker v-model:value="foreground" :modes="['hex']" />
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function useQRCode({
|
||||||
[text, background, foreground, errorCorrectionLevel].filter(isRef),
|
[text, background, foreground, errorCorrectionLevel].filter(isRef),
|
||||||
async () => {
|
async () => {
|
||||||
if (get(text))
|
if (get(text))
|
||||||
qrcode.value = await QRCode.toDataURL(get(text), {
|
qrcode.value = await QRCode.toDataURL(get(text).trim(), {
|
||||||
color: {
|
color: {
|
||||||
dark: get(foreground),
|
dark: get(foreground),
|
||||||
light: get(background),
|
light: get(background),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
|
import { isAfter, subWeeks } from 'date-fns';
|
||||||
import type { Tool } from './tools.types';
|
import type { Tool } from './tools.types';
|
||||||
|
|
||||||
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
@ -7,7 +8,10 @@ export function defineTool(
|
||||||
tool: WithOptional<Tool, 'isNew'>,
|
tool: WithOptional<Tool, 'isNew'>,
|
||||||
{ newTools }: { newTools: string[] } = { newTools: config.tools.newTools },
|
{ newTools }: { newTools: string[] } = { newTools: config.tools.newTools },
|
||||||
) {
|
) {
|
||||||
const isNew = newTools.includes(tool.name);
|
const isInNewToolConfig = newTools.includes(tool.name);
|
||||||
|
const isRecentTool = tool.createdAt ? isAfter(tool.createdAt, subWeeks(new Date(), 2)) : false;
|
||||||
|
|
||||||
|
const isNew = isInNewToolConfig || isRecentTool;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isNew,
|
isNew,
|
||||||
|
|
|
@ -9,6 +9,7 @@ export type Tool = {
|
||||||
icon: Component;
|
icon: Component;
|
||||||
redirectFrom?: string[];
|
redirectFrom?: string[];
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ToolCategory = {
|
export type ToolCategory = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue