浏览代码

Display dynamically fetched stats

Alicia Sykes 2 年之前
父节点
当前提交
391f483a05
共有 5 个文件被更改,包括 303 次插入230 次删除
  1. 66 82
      src/lib/ServiceStats.svelte
  2. 60 1
      src/lib/TemplateNotFound.svelte
  3. 3 3
      src/routes/+layout.svelte
  4. 98 9
      src/routes/[slug]/+page.server.ts
  5. 76 135
      src/routes/[slug]/+page.svelte

+ 66 - 82
src/lib/ServiceStats.svelte

@@ -7,123 +7,107 @@
 
 <div class="stats">
   {#if template.type}
-    <div class="row">
-      <span class="lbl">Type</span>
-      {#if template.type === 1}
-        <span>Container</span>
-      {:else if template.type === 2}
-        <span>Swarm</span>
-      {:else if template.type === 3}
-        <span>Kubernetes</span>
-      {:else}
-        <span>Unknown</span>
-      {/if}
-    </div>
+    <span class="lbl">Type</span>
+    {#if template.type === 1}
+      <span class="val">Container</span>
+    {:else if template.type === 2}
+      <span class="val">Swarm</span>
+    {:else if template.type === 3}
+      <span class="val">Kubernetes</span>
+    {:else}
+      <span class="val">Unknown</span>
+    {/if}
   {/if}
   {#if template.platform}
-    <div class="row">
-      <span class="lbl">Platform</span>
-      <code>{template.platform}</code>
-    </div>
+    <span class="lbl">Platform</span>
+    <code class="val">{template.platform}</code>
   {/if}
   {#if template.image}
-    <div class="row">
-      <span class="lbl">Image</span>
-      <code>{template.image}</code>
-    </div>
+    <span class="lbl">Image</span>
+    <code class="val">{template.image}</code>
   {/if}
   {#if template.command}
-    <div class="row">
-      <span class="lbl">Command</span>
-      <code>{template.command}</code>
-    </div>
+    <span class="lbl">Command</span>
+    <code class="val">{template.command}</code>
   {/if}
   {#if typeof template.interactive === 'boolean'}
-    <div class="row">
-      <span class="lbl">Interactive</span>
-      <code>{template.interactive ? 'Yes' : 'No'}</code>
-    </div>
+    <span class="lbl">Interactive</span>
+    <code class="val">{template.interactive ? 'Yes' : 'No'}</code>
   {/if}
   {#if template.ports}
-    <div class="row">
-      <span class="lbl">Ports</span>
-      <p>
-        {#each template.ports as port}<code>{port}</code>{/each}
-      </p>
-    </div>
+    <span class="lbl">Ports</span>
+    <p class="val">
+      {#each template.ports as port}<code>{port}</code>{/each}
+    </p>
   {/if}
   {#if template.volumes}
-    <div class="row">
-      <span class="lbl">Volumes</span>
-      <p>
-        {#each template.volumes as volume}<code>{volume.container || volume}</code>{/each}
-      </p>
-    </div>
+    <span class="lbl">Volumes</span>
+    <p class="val">
+      {#each template.volumes as volume}
+      <code>
+        {volume.container || volume}{volume?.bind? ' : ' + volume.bind : ''}
+      </code>{/each}
+    </p>
   {/if}
   {#if template.restart_policy}
-    <div class="row">
-      <span class="lbl">Restart Policy</span>
-      <code>{template.restart_policy}</code>
-    </div>
+    <span class="lbl">Restart Policy</span>
+    <code class="val">{template.restart_policy}</code>
   {/if}
   {#if template.repository}
-  <div class="row">
     <span class="lbl">Sourced</span>
-    <a href={template.repository.url}>Repo</a>
-  </div>
+    <a class="val" href={template.repository.url}>Repo</a>
   {/if}
   {#if template.entrypoint}
-  <div class="row">
     <span class="lbl">Entrypoint</span>
-    <code>{template.entrypoint}</code>
-  </div>
+    <code class="val">{template.entrypoint}</code>
   {/if}
   {#if template.build}
-  <div class="row">
     <span class="lbl">Build</span>
-    <code>{template.build}</code>
-  </div>
+    <code class="val">{template.build}</code>
   {/if}
   {#if template.env}
-  <div class="row">
     <span class="lbl">Env Vars</span>
-    <p>
-      {#each template.env as env}<code>{env.name}={env.set || env.value || env.default}</code>{/each}
+    <p class="val">
+      {#each template.env as env}<code>{env.name}={env.set || env.value || env.default || '\'\''}</code>{/each}
     </p>
-  </div>
   {/if}
 </div>
 
 <style lang="scss">
   .stats {
     min-width: 15rem;
-    border: 2px solid var(--background);
+    padding: 0.5rem;
+    gap: 0.5rem;
     border-radius: 6px;
-    .row {
+    display: grid;
+    grid-template-columns: 1fr auto;
+    place-items: baseline;
+    background: var(--card-2);
+
+    .lbl {
+      font-weight: 400;
+      font-style: normal;
+    }
+
+    .val {
+      max-width: 10rem;
+      overflow: hidden;
+      white-space:nowrap;
+      text-overflow: ellipsis;
+    }
+
+    span {
+      font-style: italic;
+    }
+    p {
+      margin: 0;
       display: flex;
-      justify-content: space-between;
-      flex-wrap: wrap;
-      padding: 0.5rem;
-      gap: 0.5rem;
-      &:not(:last-child) {
-        border-bottom: 2px dotted var(--background);
-      }
-      span {
-        font-style: italic;
-      }
-      p {
-        margin: 0;
-        display: flex;
-        flex-direction: column;
-      }
-      .lbl {
-        font-weight: 400;
-        font-style: normal;
-        min-width: 5rem;
-      }
-      a {
-        color: var(--accent);
-      }
+      flex-direction: column;
+    }
+
+    a {
+      color: var(--accent);
     }
   }
+  
 </style>

+ 60 - 1
src/lib/TemplateNotFound.svelte

@@ -1 +1,60 @@
-<p>Temp no Found</p>
+<script lang="ts">
+  import { gitHubRepo } from '$src/constants'; 
+  export let templateName: string;
+</script>
+
+<section>
+  <h2>Template not Found 😢</h2>
+  <p class="subtitle">It doesn't look like there was a templated named "<i>{templateName}</i>"</p>
+  <p>
+    You can try <a href="/">searching for another</a>, or if you think there's a mistake somewhere,
+    please open an issue on the <a href={gitHubRepo} target="_blank">Github Repo</a>.
+  </p>
+  <a class="back-home" href="/">Back Home</a>
+</section>
+
+<style lang="scss">
+  section {
+    background: var(--card);
+    padding: 1rem;
+    border-radius: 6px;
+    margin: 1rem auto;
+    max-width: 1000px;
+    transition: all 0.2s ease-in-out;
+    h2 {
+      margin: 0;
+      font-size: 3rem;
+      text-align: center;
+    }
+    p {
+      margin: 1rem auto;
+      font-size: 1.1rem;
+      opacity: 0.8;
+      text-align: center;
+      max-width: 40rem;
+      a {
+        color: var(--accent);
+      }
+    }
+    .back-home {
+      background: var(--background);
+      padding: 0.25rem 0.5rem;
+      margin: 0 auto;
+      display: block;
+      width: fit-content;
+      border-radius: 6px;
+      border: none;
+      color: var(--foreground);
+      font-family: Kanit;
+      font-size: 1.5rem;
+      cursor: pointer;
+      transition: all 0.2s ease-in-out;
+      text-decoration: none;
+      &:hover {
+        background: var(--gradient);
+        transform: scale(1.1) rotate(-1deg);
+      }
+    }
+    
+  }
+</style>

+ 3 - 3
src/routes/+layout.svelte

@@ -1,5 +1,4 @@
 
-
 <slot></slot>
 
 <style lang="scss">
@@ -9,6 +8,7 @@
     --foreground: #ffffff;
     --accent: #0ba5ec;
     --card: #1d2939;
+    --card-2: #192432;
     --shadow: 1px 1px 3px 3px #0B9AEC8F;
     --gradient: linear-gradient(to right,#0B9AEC 0%,#6EDFDE 100%);
     --max-width: 1800px;
@@ -18,8 +18,8 @@
     background: var(--background);
   }
   :global(::selection) {
-    background: var(--card);
-    color: var(--accent);
+    background: var(--accent);
+    color: var(--background);
   }
 
 </style>

+ 98 - 9
src/routes/[slug]/+page.server.ts

@@ -1,18 +1,107 @@
-import { get } from 'svelte/store';
+import yaml from 'js-yaml';
 
+import { get } from 'svelte/store';
 import { templatesUrl } from '$src/constants';
 import { templates } from '$src/store';
 
-export const load = async () => {
-  if (get(templates) && get(templates).length > 0) {
-    return {
-      templates: get(templates),
+/* Based on the current page name, find the corresponding template */
+const findTemplate = (templates: any, slug: string) => {
+  return templates.find((temp: Template) =>
+    temp.title.toLowerCase().replace(/[^a-zA-Z ]/g, "").replaceAll(' ', '-') === slug
+  );
+};
+
+/* With a given image name, fetch stats from DockerHub registry */
+const getDockerHubStats = async (image: string): Promise<DockerHubResponse | null> => {
+  if (!image) return null;
+  const [imageName, tag] = image.split(':');
+  const [namespace, repo] = imageName.includes('/') ? imageName.split('/') : ['library', imageName];
+  const apiEndpoint = `https://hub.docker.com/v2/repositories/${namespace}/${repo}/`;
+
+  return await fetch(apiEndpoint)
+    .then((res) => res.json())
+    .then((data) => {
+      return data;
+    })
+    .catch((err) => {
+      return null;
+    });
+}
+const getServices = async (template): Promise<Service[]> => {
+  try {
+    if (template?.repository) {
+      const { url: repoUrl, stackfile } = template.repository;
+      const path = `${repoUrl.replace('github.com', 'raw.githubusercontent.com')}/HEAD/${stackfile}`;
+      const response = await fetch(path);
+      const data = await response.text();
+      const parsedData = yaml.load(data);
+      const someServices: Service[] = [];
+      if (!parsedData.services) return [];
+
+      Object.keys(parsedData.services).forEach((service) => {
+        const serviceData = parsedData.services[service];
+        someServices.push({
+          name: service,
+          image: serviceData.image,
+          entrypoint: serviceData.entrypoint,
+          command: serviceData.command,
+          ports: serviceData.ports,
+          build: serviceData.build,
+          interactive: serviceData.interactive,
+          volumes: serviceData.volumes?.map((vol) => ({
+            bind: vol.split(':')[0],
+            container: vol.split(':')[1],
+          })),
+          restart_policy: serviceData.restart,
+          env: Object.keys(serviceData.environment || {}).map((envName) => ({
+            name: envName,
+            value: serviceData.environment[envName],
+          })),
+        });
+      });
+      return someServices;
+    } else {
+      return [];
     }
+  } catch (error) {
+    console.error('Error fetching or parsing YAML:', error);
+    return [];
+  }
+};
+
+/* Format results for returning to component */
+const returnResults = async (templates, templateSlug) => {
+  // Find template, based on slug
+  let template = findTemplate(get(templates), templateSlug);
+
+  // Fetch service info from associated stackfile, if it exists
+  let services = template?.repository ? await getServices(template) : [];
+
+  // If only 1 service, merge it with the template
+  if (services.length === 1) {
+    template = {...template, ...services[0]};
+    services = [];
+  } else if (services.length > 1) {
+    // If made up from multiple services, fetch Docker info for each image
+    services = await Promise.all(
+      services.map(async (service) => {
+        const dockerStats = await getDockerHubStats(service.image);
+        return { ...service, dockerStats };
+      })
+    );
+  }
+  // If image specified, fetch Docker image info from DockerHub
+  const dockerStats = template?.image ? await getDockerHubStats(template.image) : null;
+  return { template, dockerStats, services }
+};
+
+export const load = async ({ params }) => {
+  const templateSlug = params.slug as string;
+  if (get(templates) && get(templates).length > 0) {
+    return returnResults(templates, templateSlug);
   } else {
     const data = await fetch(templatesUrl).then((res) => res.json());
-    templates.set(data.templates);    
-    return {
-      templates: data.templates,
-    }
+    templates.set(data.templates);
+    return returnResults(templates, templateSlug);
   }
 };

+ 76 - 135
src/routes/[slug]/+page.svelte

@@ -1,95 +1,37 @@
 <script lang="ts">
-  import yaml from 'js-yaml';
 
   import { page } from '$app/stores';
-  import TemplateNotFound from '$lib/TemplateNotFound.svelte';
-  import type { Template, Service } from '$src/Types';
 
+  import Header from '$lib/Header.svelte';
+  import Footer from '$lib/Footer.svelte';
   import ServiceStats from '$lib/ServiceStats.svelte';
-  const templates = $page.data.templates as Template[];
-  const templateSlug = $page.params.slug as string;
-  
-  const template = templates.find((temp: Template) =>
-    temp.title.toLowerCase().replace(/[^a-zA-Z ]/g, "").replaceAll(' ', '-') === templateSlug
-  );
-
-  console.log(template);
-
-
-type Service = {
-  name: string;
-  image: string;
-  entrypoint: string;
-  command: string;
-  ports: string[];
-  build: string;
-  interactive: boolean;
-  volumes: { bind: string; container: string }[];
-  restart_policy: string;
-  environment: { name: string; value: string }[];
-};
-
-const getServices = async (): Promise<Service[]> => {
-  try {
-    if (template?.repository) {
-      const { url: repoUrl, stackfile } = template.repository;
-      const path = `${repoUrl.replace(
-        'github.com',
-        'raw.githubusercontent.com'
-      )}/HEAD/${stackfile}`;
-      const response = await fetch(path);
-      const data = await response.text();
-      const parsedData = yaml.load(data);
-      const someServices: Service[] = [];
-      if (!parsedData.services) return [];
-
-      console.log(parsedData);
-      Object.keys(parsedData.services).forEach((service) => {
-        const serviceData = parsedData.services[service];
-        someServices.push({
-          name: service,
-          image: serviceData.image,
-          entrypoint: serviceData.entrypoint,
-          command: serviceData.command,
-          ports: serviceData.ports,
-          build: serviceData.build,
-          interactive: serviceData.interactive,
-          volumes: serviceData.volumes?.map((vol) => ({
-            bind: vol.split(':')[0],
-            container: vol.split(':')[1],
-          })),
-          restart_policy: serviceData.restart,
-          env: Object.keys(serviceData.environment || {}).map((envName) => ({
-            name: envName,
-            value: serviceData.environment[envName],
-          })),
-        });
-      });
-      console.log(someServices);
-      return someServices;
-    } else {
-      return [];
-    }
-  } catch (error) {
-    console.error('Error fetching or parsing YAML:', error);
-    return [];
-  }
-};
-
-const services: Service[] = getServices();
+  import TemplateNotFound from '$lib/TemplateNotFound.svelte';
+  import DockerStats from '$lib/DockerStats.svelte';
+  import MdContent from '$lib/MdContent.svelte';
+  import InstallationInstructions from '$lib/InstallationInstructions.svelte';
+
+  import type { Template, Service, DockerHubResponse } from '$src/Types';
+
+  const urlSlug = $page.params.slug;
+  const template = $page.data.template as Template;
+  const dockerStats = $page.data.dockerStats as DockerHubResponse;
+  const services = $page.data.services as Service[];
+  const serviceDockerStats = $page.data.serviceDockerStats as DockerHubResponse[] || null;
+
+  const makeMultiDoc = (services: Service[]) => {
+    return services.map((s) => {
+      return s?.dockerStats?.full_description ? {
+        name: s.name,
+        description: s.dockerStats.description,
+        content: s.dockerStats.full_description,
+        visible: false,
+      } : null;
+    }).filter((thingy) => thingy !== null);
+  };
 
 </script>
 
-<header>
-  <a class="title" href="/">
-    <img src="https://i.ibb.co/hMymwH0/portainer-templates-small.png" />
-    <h2>Portainer Templates</h2>
-  </a>
-  <nav>
-    <a href="/">Home</a>
-    <a href="https://github.com/lissy93/portainer-templates">View on GitHub</a>
-  </nav>
-</header>
+<Header />
 
 {#if template}
   <section class="summary-section">
@@ -105,7 +47,14 @@ const services: Service[] = getServices();
       </p>
     {/if}
     <div class="content">
-      <p class="description">{template.description}</p>
+      <div class="left">
+        <p class="description">{template.description}</p>
+        {#await template then returnedTemplate}
+          {#if dockerStats && dockerStats.name}
+          <DockerStats info={dockerStats} />
+          {/if}
+        {/await}
+      </div>
       <ServiceStats template={template} />
     </div>
   </section>
@@ -116,67 +65,44 @@ const services: Service[] = getServices();
       <h2>Services</h2>
       <div class="service-list">
         {#each returnedServices as service}
-          <div>
-            <h3>{service.name}</h3>
+          <div class="service-each">
+          <h3>{service.name}</h3>  
+          <div class="service-data">
             <ServiceStats template={service} />
+            {#if service.dockerStats && service.dockerStats.name}
+              <DockerStats info={service.dockerStats} />
+            {/if}
           </div>
+        </div>
         {/each}
       </div>
     </section>
   {/if}
   {/await}
+
+  <InstallationInstructions portainerTemplate={template} portainerServices={services || null} />
+
+  {#if dockerStats?.full_description}
+    <MdContent content={dockerStats.full_description} />
+  {:else if services.length > 0}
+    <MdContent multiContent={makeMultiDoc(services)} />
+  {/if}
+
 {:else}
-  <TemplateNotFound />
+  <TemplateNotFound templateName={urlSlug} />
 {/if}
 
-<style lang="scss">
-  header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    background: var(--card);
-    padding: 0.25rem 1rem;
-    a.title {
-      display: flex;
-      justify-content: center;
-      gap: 1rem;
-      color: var(--foreground);
-      text-decoration: none;
-      h2 {
-        margin: 0;
-        font-size: 1.5rem;
-        font-weight: 600;
-      }
-      img {
-        width: 40px;
-        transition: all 0.3s ease-in-out;
-      }
-      &:hover {
-        img { transform: rotate(-5deg) scale(1.1); }
-      }
-    }
+<Footer />
 
-    nav {
-      display: flex;
-      gap: 1rem;
-      a {
-        color: var(--foreground);
-        text-decoration: none;
-        padding: 0.25rem 0.5rem;
-        border-radius: 6px;
-        transition: all 250ms ease-in-out;
-        &:hover {
-          background: var(--gradient);
-          transform: scale(1.05);
-        }
-      }
-    }
+<style lang="scss">
+  section {
+    max-width: 1000px;
+    margin: 1rem auto;
   }
   .summary-section {
     background: var(--card);
     border-radius: 6px;
     padding: 1rem;
-    margin: 1rem;
     display: flex;
     flex-direction: column;
     h1 {
@@ -212,15 +138,24 @@ const services: Service[] = getServices();
     flex-wrap: wrap;
     gap: 1rem;
     justify-content: space-between;
+    margin-top: 1rem;
+    .left {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      gap: 1rem;
+    }
     p.description {
-      max-width: 60%;
+      background: var(--card-2);
+      padding: 1rem;
+      border-radius: 6px;
+      margin: 0;
     }
   }
 
   .service-section {
     background: var(--card);
-    border-radius: 6px;
-    margin: 1rem;
+    border-radius: 6px; 
     padding: 1rem;
     h2 {
       margin: 0;
@@ -228,12 +163,18 @@ const services: Service[] = getServices();
     }
     .service-list {
       display: flex;
-      gap: 1rem;
-      // justify-content: space-between;
+      gap: 2rem;
       flex-wrap: wrap;
       h3 {
         margin: 0.5rem 0;
         font-weight: 400;
+        font-size: 2rem;
+      }
+      .service-each {
+        .service-data {
+          display: flex;
+          gap: 1rem;
+        }
       }
     }
   }