diff --git a/scripts/create-tool.mjs b/scripts/create-tool.mjs index 33ab807c..36a20d8e 100644 --- a/scripts/create-tool.mjs +++ b/scripts/create-tool.mjs @@ -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]}'), }); `, ); diff --git a/src/tools/benchmark-builder/benchmark-builder.models.ts b/src/tools/benchmark-builder/benchmark-builder.models.ts new file mode 100644 index 00000000..be8f9658 --- /dev/null +++ b/src/tools/benchmark-builder/benchmark-builder.models.ts @@ -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 }) { + 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}`; +} diff --git a/src/tools/benchmark-builder/benchmark-builder.vue b/src/tools/benchmark-builder/benchmark-builder.vue new file mode 100644 index 00000000..487a97f4 --- /dev/null +++ b/src/tools/benchmark-builder/benchmark-builder.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/tools/benchmark-builder/dynamic-values.vue b/src/tools/benchmark-builder/dynamic-values.vue new file mode 100644 index 00000000..70268aed --- /dev/null +++ b/src/tools/benchmark-builder/dynamic-values.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/tools/benchmark-builder/index.ts b/src/tools/benchmark-builder/index.ts new file mode 100644 index 00000000..b9dcf114 --- /dev/null +++ b/src/tools/benchmark-builder/index.ts @@ -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, +}); diff --git a/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue b/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue index aeacecd2..a106878d 100644 --- a/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue +++ b/src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue @@ -22,7 +22,9 @@

- {{ notComposable }} +
    +
  • {{ message }}
  • +
@@ -32,14 +34,18 @@ title="This options are not yet implemented and therefore haven't been translated to docker-compose" type="warning" > - {{ notImplemented }} +

- {{ errors }} +
    +
  • {{ message }}
  • +
@@ -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('
'), + 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('
'), + 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('
'), + .map((msg) => msg.value), ); const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value)); const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); diff --git a/src/tools/index.ts b/src/tools/index.ts index 2a213011..29d32422 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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', diff --git a/src/tools/qr-code-generator/qr-code-generator.vue b/src/tools/qr-code-generator/qr-code-generator.vue index e565ba7e..9800f283 100644 --- a/src/tools/qr-code-generator/qr-code-generator.vue +++ b/src/tools/qr-code-generator/qr-code-generator.vue @@ -4,7 +4,12 @@ - + diff --git a/src/tools/qr-code-generator/useQRCode.ts b/src/tools/qr-code-generator/useQRCode.ts index 44b72a72..64ee90a1 100644 --- a/src/tools/qr-code-generator/useQRCode.ts +++ b/src/tools/qr-code-generator/useQRCode.ts @@ -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), diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 8289aa33..a5d157e9 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,4 +1,5 @@ import { config } from '@/config'; +import { isAfter, subWeeks } from 'date-fns'; import type { Tool } from './tools.types'; type WithOptional = Omit & Partial>; @@ -7,7 +8,10 @@ export function defineTool( tool: WithOptional, { 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, diff --git a/src/tools/tools.types.ts b/src/tools/tools.types.ts index 5630a12e..48f60629 100644 --- a/src/tools/tools.types.ts +++ b/src/tools/tools.types.ts @@ -9,6 +9,7 @@ export type Tool = { icon: Component; redirectFrom?: string[]; isNew: boolean; + createdAt?: Date; }; export type ToolCategory = {