{"id":2259,"date":"2022-01-25T18:17:30","date_gmt":"2022-01-25T18:17:30","guid":{"rendered":"https:\/\/lvboard.infostore.in.ua\/?p=2259"},"modified":"2022-01-25T18:17:30","modified_gmt":"2022-01-25T18:17:30","slug":"building-a-react-dashboard-to-visualize-workflow-and-job-events","status":"publish","type":"post","link":"https:\/\/lvboard.infostore.in.ua\/?p=2259","title":{"rendered":"Building a React dashboard to visualize workflow and job events"},"content":{"rendered":"\n<p>Data visualization is the process of translating large data sets and metrics into charts, graphs, and other visuals. <\/p>\n\n\n\n<!--more-->\n\n\n\n<p>The resulting visual representation of data makes it easier to identify and share real-time trends, outliers, and new insights about the information represented in the data. Using CircleCI\u00a0<a href=\"https:\/\/circleci.com\/docs\/2.0\/webhooks\/\">webhooks<\/a>, we can gather data on workflow and job events. In this tutorial, I will lead you through the steps to create a React-based dashboard to visualize this data.<\/p>\n\n\n\n<h2>Prerequisites<\/h2>\n\n\n\n<p>To follow along with this tutorial, you will need a good grasp of JavaScript ES6. You will also need to have an understanding of some basic React concepts like hooks and functional components.<\/p>\n\n\n\n<p>You will need to have these installed on your workstation:<\/p>\n\n\n\n<ol><li>An up-to-date installation of&nbsp;<a href=\"https:\/\/nodejs.org\/en\/download\/\" target=\"_blank\" rel=\"noreferrer noopener\">node.js<\/a>, and a package manager like&nbsp;<a href=\"https:\/\/www.npmjs.com\/get-npm\" target=\"_blank\" rel=\"noreferrer noopener\">npm<\/a>&nbsp;or&nbsp;<a href=\"https:\/\/yarnpkg.com\/getting-started\/install#\" target=\"_blank\" rel=\"noreferrer noopener\">yarn<\/a><\/li><li>Your preferred code editor<\/li><li>An API communicating with your CircleCI webhook. You can read about setting one up&nbsp;<a href=\"https:\/\/github.com\/yemiwebby\/circle_ci_webhook\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/li><li>Ensure that the&nbsp;<a href=\"https:\/\/github.com\/CIRCLECI-GWP\/laravel-api-circleci-webhook\" target=\"_blank\" rel=\"noreferrer noopener\">Laravel API CircleCI webhook<\/a>&nbsp;covered in&nbsp;<a href=\"https:\/\/circleci.com\/blog\/building-a-laravel-api-for-circleci-webhooks\">this tutorial<\/a>&nbsp;is up and running.<\/li><\/ol>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>Our tutorials are platform-agnostic, but use CircleCI as an example. If you don\u2019t have a CircleCI account,&nbsp;<strong><a href=\"https:\/\/circleci.com\/signup\/\">sign up for a free one here.<\/a><\/strong><\/p><\/blockquote>\n\n\n\n<h2>Getting started<\/h2>\n\n\n\n<p>Create a new React application using this command:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>yarn create react-app circleci_workflow_dashboard\n\ncd circleci_workflow_dashboard\n<\/code><\/pre>\n\n\n\n<p>For this project, we will be using&nbsp;<a href=\"https:\/\/ant.design\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ant Design<\/a>&nbsp;for laying out the UI and&nbsp;<a href=\"https:\/\/github.com\/reactchartjs\/react-chartjs-2\" target=\"_blank\" rel=\"noreferrer noopener\">react-chartjs-2<\/a>&nbsp;to display our charts.<\/p>\n\n\n\n<p>Add the project dependencies using yarn:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>yarn add antd react-chartjs-2@3.3.0 chart.js\n<\/code><\/pre>\n\n\n\n<p>The next step is to import the styling for ant design. Update&nbsp;<code>src\/App.css<\/code>&nbsp;with this:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@import '~antd\/dist\/antd.css';\n<\/code><\/pre>\n\n\n\n<h2>Adding utility functionality<\/h2>\n\n\n\n<p>Next, we need a means of communicating with our API. In the&nbsp;<code>src<\/code>&nbsp;folder, create a new folder called&nbsp;<code>utility<\/code>. In the&nbsp;<code>utility<\/code>&nbsp;folder, create a new file named&nbsp;<code>API.js<\/code>. In the&nbsp;<code>src\/utility\/API.js<\/code>&nbsp;file, add this:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export const makeGETRequest = (url) =&gt; {\n  return fetch(url, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application\/json\",\n    },\n  }).then((response) =&gt; response.json());\n};\n<\/code><\/pre>\n\n\n\n<p>Using this function, we can make a&nbsp;<code>GET<\/code>&nbsp;request to the API. The function returns the JSON response from the API.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>For the sake of simplicity, error handling is not included in the function.<\/p><\/blockquote>\n\n\n\n<p>To represent dates in a readable format, we can create a utility function. In the&nbsp;<code>utility<\/code>&nbsp;folder, create a new file called&nbsp;<code>Date.js<\/code>&nbsp;and add this:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export const formatDate = (date) =&gt;\n  new Date(date).toLocaleString(\"en-GB\", {\n    month: \"long\",\n    weekday: \"long\",\n    day: \"numeric\",\n    year: \"numeric\",\n  });\n<\/code><\/pre>\n\n\n\n<h2>Building dashboard components<\/h2>\n\n\n\n<p>Our dashboard will display a table of events you can filter on status and event type. You will also be able to toggle the dashboard between a tabular display and a chart display.<\/p>\n\n\n\n<p>In the&nbsp;<code>src<\/code>&nbsp;folder, create a new folder called&nbsp;<code>components<\/code>. This folder will hold all the custom components we will create.<\/p>\n\n\n\n<p>In the&nbsp;<code>components<\/code>&nbsp;directory, create a new file called&nbsp;<code>StatusTag.jsx<\/code>. This component will be used to display a tag with a color determined by the event\u2019s status. Add this to&nbsp;<code>StatusTag.jsx<\/code>:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport { Tag } from \"antd\";\n\nconst StatusTag = ({ status }) =&gt; {\n  const statusColours = {\n    success: \"green\",\n    failed: \"volcano\",\n    error: \"red\",\n    canceled: \"orange\",\n    unauthorized: \"magenta\",\n  };\n  return &lt;Tag color={statusColours&#91;status]}&gt;{status}&lt;\/Tag&gt;;\n};\n\nexport default StatusTag;\n<\/code><\/pre>\n\n\n\n<p>Next, create a new file in the&nbsp;<code>components<\/code>&nbsp;directory named&nbsp;<code>TableView.jsx<\/code>. This component will take the notifications as a prop and render them in a table with the ability to filter based on type and status. Add this code to&nbsp;<code>TableView.jsx<\/code>:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport { Table } from \"antd\";\nimport { formatDate } from \"..\/utility\/Date\";\nimport StatusTag from \".\/StatusTag\";\n\nconst TableView = ({ notifications }) =&gt; {\n  const filterHandler = (value, record, key) =&gt; record&#91;key] === value;\n\n  const columns = &#91;\n    {\n      title: \"Subject\",\n      dataIndex: \"commit_subject\",\n    },\n    {\n      title: \"Commit Author\",\n      dataIndex: \"commit_author\",\n    },\n    {\n      title: \"Happened At\",\n      dataIndex: \"happened_at\",\n      render: (text) =&gt; formatDate(text),\n    },\n    {\n      title: \"Event Type\",\n      dataIndex: \"type\",\n      filters: &#91;\n        { text: \"Job\", value: \"job-completed\" },\n        { text: \"Workflow\", value: \"workflow-completed\" },\n      ],\n      onFilter: (value, record) =&gt; filterHandler(value, record, \"type\"),\n    },\n    {\n      title: \"Event Status\",\n      dataIndex: \"event_status\",\n      filters: &#91;\n        { text: \"Success\", value: \"success\" },\n        { text: \"Failed\", value: \"failed\" },\n        { text: \"Error\", value: \"error\" },\n        { text: \"Canceled\", value: \"canceled\" },\n        { text: \"Unauthorized\", value: \"unauthorized\" },\n      ],\n      render: (text) =&gt; &lt;StatusTag status={text} \/&gt;,\n      onFilter: (value, record) =&gt; filterHandler(value, record, \"event_status\"),\n    },\n    {\n      title: \"Notification ID\",\n      dataIndex: \"notification_id\",\n      render: (text, record) =&gt; (\n        &lt;a href={record&#91;\"workflow_url\"]} target=\"_blank\" rel=\"noreferrer\"&gt;\n          {text}\n        &lt;\/a&gt;\n      ),\n    },\n  ];\n\n  return (\n    &lt;Table dataSource={notifications} columns={columns} rowKey=\"id\" bordered \/&gt;\n  );\n};\n\nexport default TableView;\n<\/code><\/pre>\n\n\n\n<p>The table has six columns: Subject, Commit Author, Happened At, Event Type, Event Status, and Notification ID.<\/p>\n\n\n\n<p>Each column is represented by an object in the&nbsp;<code>columns<\/code>&nbsp;constant. Where no special rendering is required, the&nbsp;<code>title<\/code>&nbsp;and&nbsp;<code>dataIndex<\/code>&nbsp;are sufficient for the column declaration. The&nbsp;<code>title<\/code>&nbsp;is used as the title of the column while the&nbsp;<code>dataIndex<\/code>&nbsp;entry lets antd know which property the column is populated with.<\/p>\n\n\n\n<p>To specify the component or JSX element to be rendered in a column, we add a&nbsp;<code>render<\/code>&nbsp;key to the column\u2019s object representation. We use this to render the&nbsp;<code>StatusTag<\/code>&nbsp;component we created earlier. We also use this to render a well-formatted date for the&nbsp;<code>Happened At<\/code>&nbsp;column and the Notification ID as a link to the workflow.<\/p>\n\n\n\n<h2>Building chart components<\/h2>\n\n\n\n<p>For this tutorial, we will render the data in three charts:<\/p>\n\n\n\n<ol><li>A pie chart showing the distribution of events by status: success, failed, error, canceled, or unauthorized.<\/li><li>A bar chart showing the distribution of events by type: either workflow or job<\/li><li>A line chart showing the timeline of events<\/li><\/ol>\n\n\n\n<p>To build the pie chart, create a new file in the&nbsp;<code>components<\/code>&nbsp;folder called&nbsp;<code>StatusDistribution.jsx<\/code>&nbsp;and add this code to it:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport { Pie } from \"react-chartjs-2\";\n\nconst StatusDistribution = ({ notifications }) =&gt; {\n  const sortedNotifications = notifications.reduce(\n    (sortedNotifications, notification) =&gt; {\n      sortedNotifications&#91;notification&#91;\"event_status\"]]++;\n      return sortedNotifications;\n    },\n    { error: 0, failed: 0, success: 0, unauthorized: 0, canceled: 0 }\n  );\n\n  const data = {\n    labels: &#91;\"Error\", \"Failed\", \"Success\", \"Unauthorized\", \"Canceled\"],\n    datasets: &#91;\n      {\n        data: Object.values(sortedNotifications),\n        backgroundColor: &#91;\n          \"rgba(255, 99, 132, 0.2)\",\n          \"rgba(255, 206, 86, 0.2)\",\n          \"rgba(75, 192, 192, 0.2)\",\n          \"rgba(153, 102, 255, 0.2)\",\n          \"rgba(255, 159, 64, 0.2)\",\n        ],\n        borderColor: &#91;\n          \"rgba(255, 99, 132, 1)\",\n          \"rgba(255, 206, 86, 1)\",\n          \"rgba(75, 192, 192, 1)\",\n          \"rgba(153, 102, 255, 1)\",\n          \"rgba(255, 159, 64, 1)\",\n        ],\n        borderWidth: 1,\n      },\n    ],\n  };\n\n  return (\n    &lt;&gt;\n      &lt;div className=\"header\"&gt;\n        &lt;h1 className=\"title\"&gt;Status Distribution&lt;\/h1&gt;\n      &lt;\/div&gt;\n      &lt;Pie data={data} height={50} \/&gt;\n    &lt;\/&gt;\n  );\n};\n\nexport default StatusDistribution;\n<\/code><\/pre>\n\n\n\n<p>Using the&nbsp;<code>reduce<\/code>&nbsp;function on the notifications array, the notifications are sorted and counted according to the status. The values are then passed to the&nbsp;<code>data<\/code>&nbsp;key in the&nbsp;<code>datasets<\/code>&nbsp;configuration, which is passed to the&nbsp;<code>Pie<\/code>&nbsp;component.<\/p>\n\n\n\n<p>To build the bar chart, create a new file in the&nbsp;<code>components<\/code>&nbsp;directory called&nbsp;<code>TypeDistribution.jsx<\/code>&nbsp;and add this code to it.<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport { Bar } from \"react-chartjs-2\";\n\nconst TypeDistribution = ({ notifications }) =&gt; {\n  const sortedNotifications = notifications.reduce(\n    (sortedNotifications, notification) =&gt; {\n      sortedNotifications&#91;notification&#91;\"type\"]]++;\n      return sortedNotifications;\n    },\n    { \"job-completed\": 0, \"workflow-completed\": 0 }\n  );\n\n  const data = {\n    labels: &#91;\"Job\", \"Workflow\"],\n    datasets: &#91;\n      {\n        label: \"Event Type\",\n        data: Object.values(sortedNotifications),\n        backgroundColor: &#91;\"rgba(54, 162, 235, 0.2)\", \"rgba(75, 192, 192, 0.2)\"],\n        borderColor: &#91;\"rgba(54, 162, 235, 1)\", \"rgba(75, 192, 192, 1)\"],\n        borderWidth: 1,\n      },\n    ],\n  };\n\n  const options = {\n    scales: {\n      y: {\n        beginAtZero: true,\n      },\n    },\n  };\n\n  return (\n    &lt;&gt;\n      &lt;div className=\"header\"&gt;\n        &lt;h1 className=\"title\"&gt;Type Distribution&lt;\/h1&gt;\n      &lt;\/div&gt;\n      &lt;Bar data={data} options={options} height={500} \/&gt;\n    &lt;\/&gt;\n  );\n};\n\nexport default TypeDistribution;\n<\/code><\/pre>\n\n\n\n<p>Just as we did for the status distribution chart, we sort and count the notifications based on the notification type. The values are then passed to the&nbsp;<code>data<\/code>&nbsp;key in the&nbsp;<code>datasets<\/code>&nbsp;configuration, which is passed to the&nbsp;<code>Bar<\/code>&nbsp;component.<\/p>\n\n\n\n<p>To build the timeline of notifications, create a new file in the&nbsp;<code>components<\/code>&nbsp;directory called&nbsp;<code>Timeline.jsx<\/code>&nbsp;and add this code to it:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport { Line } from \"react-chartjs-2\";\nimport { formatDate } from \"..\/utility\/Date\";\n\nconst Timeline = ({ notifications }) =&gt; {\n  const sortedNotifications = notifications.reduce(\n    (sortedNotifications, notification) =&gt; {\n      const notificationDate = formatDate(notification&#91;\"happened_at\"]);\n      if (notificationDate in sortedNotifications) {\n        sortedNotifications&#91;notificationDate]++;\n      } else {\n        sortedNotifications&#91;notificationDate] = 1;\n      }\n      return sortedNotifications;\n    },\n    {}\n  );\n\n  const data = {\n    labels: Object.keys(sortedNotifications),\n    datasets: &#91;\n      {\n        label: \"Number of events\",\n        data: Object.values(sortedNotifications),\n        fill: false,\n        backgroundColor: \"rgb(255, 99, 132)\",\n        borderColor: \"rgba(255, 99, 132, 0.2)\",\n      },\n    ],\n  };\n\n  const options = {\n    scales: {\n      y: {\n        beginAtZero: true,\n      },\n    },\n  };\n  return (\n    &lt;&gt;\n      &lt;div className=\"header\"&gt;\n        &lt;h1 className=\"title\"&gt;Event Timeline&lt;\/h1&gt;\n      &lt;\/div&gt;\n      &lt;Line data={data} options={options} height={500} width={1500} \/&gt;\n    &lt;\/&gt;\n  );\n};\n\nexport default Timeline;\n<\/code><\/pre>\n\n\n\n<p>The sorting functionality for this component is slightly different. Because we cannot specify all the possible dates in an initial object, we start with an empty object. Then, for each notification, we check if a key already exists for that date. If it does, we increase the count, if not we add the date with a value of 1.<\/p>\n\n\n\n<p>Next, we need to build a component that renders all the charts in a grid. Create a new file in the&nbsp;<code>components<\/code>&nbsp;directory called&nbsp;<code>ChartView.jsx<\/code>&nbsp;and add this code to it:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from \"react\";\nimport StatusDistribution from \".\/StatusDistribution\";\nimport { Col, Row } from \"antd\";\nimport TypeDistribution from \".\/TypeDistribution\";\nimport Timeline from \".\/Timeline\";\n\nconst ChartView = ({ notifications }) =&gt; {\n  return (\n    &lt;&gt;\n      &lt;Timeline notifications={notifications} \/&gt;\n      &lt;Row style={{ marginTop: \"30px\" }} gutter={96}&gt;\n        &lt;Col&gt;\n          &lt;StatusDistribution notifications={notifications} \/&gt;\n        &lt;\/Col&gt;\n        &lt;Col offset={6}&gt;\n          &lt;TypeDistribution notifications={notifications} \/&gt;\n        &lt;\/Col&gt;\n      &lt;\/Row&gt;\n    &lt;\/&gt;\n  );\n};\n\nexport default ChartView;\n<\/code><\/pre>\n\n\n\n<p>In this component, we render the status distribution and type distribution side-by-side with the timeline above.<\/p>\n\n\n\n<p>Passing the notifications as we have done here is known as&nbsp;<a href=\"https:\/\/kentcdodds.com\/blog\/prop-drilling\" target=\"_blank\" rel=\"noreferrer noopener\">prop drilling<\/a>. Although it is generally discouraged. we did it here for the sake of simplifying the tutorial. In production apps, you should consider a proper state management implementation.<\/p>\n\n\n\n<h2>Putting it all together<\/h2>\n\n\n\n<p>With all the child components in place, update&nbsp;<code>src\/App.js<\/code>&nbsp;to match this:<a><\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import \".\/App.css\";\nimport { makeGETRequest } from \".\/utility\/Api\";\nimport { useEffect, useState } from \"react\";\nimport { Card, Col, Row, Switch } from \"antd\";\nimport TableView from \".\/components\/TableView\";\nimport ChartView from \".\/components\/ChartView\";\n\nconst App = () =&gt; {\n  const &#91;notifications, setNotifications] = useState(&#91;]);\n  const &#91;showTableView, setShowTableView] = useState(false);\n\n  useEffect(() =&gt; {\n    makeGETRequest(\"http:\/\/127.0.0.1:8000\/api\/circleci\").then((response) =&gt; {\n      setNotifications(response);\n      console.log(response);\n    });\n  }, &#91;]);\n\n  const handleSwitchValueChange = () =&gt; {\n    setShowTableView((showTableView) =&gt; !showTableView);\n  };\n\n  return (\n    &lt;Card style={{ margin: \"2%\" }}&gt;\n      &lt;Row style={{ marginBottom: \"10px\" }}&gt;\n        &lt;Col span={6} offset={18}&gt;\n          Show Data as Table\n          &lt;Switch checked={showTableView} onChange={handleSwitchValueChange} \/&gt;\n        &lt;\/Col&gt;\n      &lt;\/Row&gt;\n      {showTableView ? (\n        &lt;TableView notifications={notifications} \/&gt;\n      ) : (\n        &lt;ChartView notifications={notifications} \/&gt;\n      )}\n    &lt;\/Card&gt;\n  );\n};\n\nexport default App;\n<\/code><\/pre>\n\n\n\n<p>In this component, we retrieve the notifications from our API in the&nbsp;<code>useEffect<\/code>&nbsp;hook and save it to state using the&nbsp;<code>setNotifications<\/code>&nbsp;function. We then declare a function to handle the toggling of the&nbsp;<code>showTableView<\/code>&nbsp;state variable, which determines whether the data is displayed in a table or chart.<\/p>\n\n\n\n<p>To toggle between views, we render a&nbsp;<code>Switch<\/code>&nbsp;and pass it the&nbsp;<code>showTableView<\/code>&nbsp;and&nbsp;<code>handleSwitchValueChange<\/code>&nbsp;values as props.<\/p>\n\n\n\n<p>Finally, depending on the value of&nbsp;<code>showTableView<\/code>, either the&nbsp;<code>TableView<\/code>&nbsp;or&nbsp;<code>ChartView<\/code>&nbsp;component is rendered.<\/p>\n\n\n\n<h3>Table view<\/h3>\n\n\n\n<figure class=\"wp-block-image\"><img src=\"https:\/\/production-cci-com.imgix.net\/blog\/media\/2022-01-07-react-dashboard.png?ixlib=rb-3.2.1&amp;w=2000&amp;auto=format&amp;fit=max&amp;q=60&amp;ch=DPR%2CWidth%2CViewport-Width%2CSave-Data\" alt=\"Dashboard displaying table view\"\/><\/figure>\n\n\n\n<h3>Chart view<\/h3>\n\n\n\n<figure class=\"wp-block-image\"><img src=\"https:\/\/production-cci-com.imgix.net\/blog\/media\/2022-01-07-table-view.png?ixlib=rb-3.2.1&amp;w=2000&amp;auto=format&amp;fit=max&amp;q=60&amp;ch=DPR%2CWidth%2CViewport-Width%2CSave-Data\" alt=\"Dashboard displaying chart view\"\/><\/figure>\n\n\n\n<h2>Conclusion<\/h2>\n\n\n\n<p>In this tutorial, we examined how we can build a React dashboard to visualize pipeline events using an API. By visualizing data in charts, we can get a high-level understanding of our pipeline and also make sense of the dataset, no matter how large. While we did not implement this feature, you can also export the data into spreadsheets for further analysis. Share this sample project with your team and extend your learning!<\/p>\n\n\n\n<p>The code for this article is available on&nbsp;<a href=\"https:\/\/github.com\/yemiwebby\/circleci_board\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a>&nbsp;and the complete documentation for CircleCI webhooks is available&nbsp;<a href=\"https:\/\/circleci.com\/docs\/2.0\/webhooks\/\">here<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><a href=\"https:\/\/twitter.com\/yemiwebby\" target=\"_blank\" rel=\"noreferrer noopener\">Oluyemi<\/a>&nbsp;is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile. A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Data visualization is the process of translating large data sets and metrics into charts, graphs, and other visuals.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[30],"tags":[177,65],"_links":{"self":[{"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/posts\/2259"}],"collection":[{"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2259"}],"version-history":[{"count":1,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/posts\/2259\/revisions"}],"predecessor-version":[{"id":2260,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=\/wp\/v2\/posts\/2259\/revisions\/2260"}],"wp:attachment":[{"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2259"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2259"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lvboard.infostore.in.ua\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2259"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}