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
					
				| @ -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…
	
		Reference in New Issue
	
	Block a user