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("', '")}'],
|
||||
component: () => import('./${toolName}.vue'),
|
||||
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">
|
||||
<br />
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -32,14 +34,18 @@
|
|||
title="This options are not yet implemented and therefore haven't been translated to docker-compose"
|
||||
type="warning"
|
||||
>
|
||||
{{ notImplemented }}
|
||||
<ul>
|
||||
<li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.length > 0">
|
||||
<br />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,22 +69,15 @@ const conversionResult = computed(() =>
|
|||
);
|
||||
const dockerCompose = computed(() => conversionResult.value.yaml);
|
||||
const notImplemented = computed(() =>
|
||||
conversionResult.value.messages
|
||||
.filter((msg) => msg.type === MessageType.notImplemented)
|
||||
.map((msg) => msg.value)
|
||||
.join('<br>'),
|
||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value),
|
||||
);
|
||||
const notComposable = computed(() =>
|
||||
conversionResult.value.messages
|
||||
.filter((msg) => msg.type === MessageType.notTranslatable)
|
||||
.map((msg) => msg.value)
|
||||
.join('<br>'),
|
||||
conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value),
|
||||
);
|
||||
const errors = computed(() =>
|
||||
conversionResult.value.messages
|
||||
.filter((msg) => msg.type === MessageType.errorDuringConversion)
|
||||
.map((msg) => msg.value)
|
||||
.join('<br>'),
|
||||
.map((msg) => msg.value),
|
||||
);
|
||||
const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value));
|
||||
const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' });
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { tool as base64FileConverter } from './base64-file-converter';
|
||||
import { tool as base64StringConverter } from './base64-string-converter';
|
||||
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 dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter';
|
||||
import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor';
|
||||
|
@ -107,7 +108,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
},
|
||||
{
|
||||
name: 'Measurement',
|
||||
components: [chronometer, temperatureConverter],
|
||||
components: [chronometer, temperatureConverter, benchmarkBuilder],
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
<n-gi span="2">
|
||||
<n-form label-width="130" label-placement="left">
|
||||
<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 label="Foreground color:">
|
||||
<n-color-picker v-model:value="foreground" :modes="['hex']" />
|
||||
|
|
|
@ -19,7 +19,7 @@ export function useQRCode({
|
|||
[text, background, foreground, errorCorrectionLevel].filter(isRef),
|
||||
async () => {
|
||||
if (get(text))
|
||||
qrcode.value = await QRCode.toDataURL(get(text), {
|
||||
qrcode.value = await QRCode.toDataURL(get(text).trim(), {
|
||||
color: {
|
||||
dark: get(foreground),
|
||||
light: get(background),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { config } from '@/config';
|
||||
import { isAfter, subWeeks } from 'date-fns';
|
||||
import type { Tool } from './tools.types';
|
||||
|
||||
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'>,
|
||||
{ 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 {
|
||||
isNew,
|
||||
|
|
|
@ -9,6 +9,7 @@ export type Tool = {
|
|||
icon: Component;
|
||||
redirectFrom?: string[];
|
||||
isNew: boolean;
|
||||
createdAt?: Date;
|
||||
};
|
||||
|
||||
export type ToolCategory = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue