diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_chart.stories.js b/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_chart.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..a11d9062bb72bc6f842ea19782421178c19dcb26
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_chart.stories.js
@@ -0,0 +1,71 @@
+import Scatterplot from '../../shared/components/scatterplot.vue';
+import MetricChart from './metric_chart.vue';
+import { metricTypes, metricType } from './stories_constants';
+
+export default {
+ component: MetricChart,
+ title: 'ee/analytics/productivity_analytics/components/metric_chart',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MetricChart },
+ props: Object.keys(argTypes),
+ template: `
+ `,
+});
+
+const WithScatterplot = (args, { argTypes }) => ({
+ components: { MetricChart, Scatterplot },
+ props: Object.keys(argTypes),
+ template: `
+
+
+
+ `,
+});
+
+const scatterplotMainData = [
+ ['2024-02-18', 24, '2024-02-18T12:08:17.000Z'],
+ ['2024-02-25', 3, '2024-02-25T10:00:05.000Z'],
+ ['2024-03-03', 13, '2024-03-03T11:19:55.000Z'],
+];
+
+const defaultArgs = {
+ title: 'Fake metric chart',
+ description: 'This is a fake metric chart, used for testing',
+ isLoading: false,
+ chartData: [[24, 13]],
+ metricTypes,
+ errorCode: null,
+ selectedMetric: '',
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ ...defaultArgs,
+};
+
+export const SelectedMetric = Template.bind({});
+SelectedMetric.args = {
+ ...defaultArgs,
+ selectedMetric: metricType,
+};
+
+export const Loading = Template.bind({});
+Loading.args = {
+ ...defaultArgs,
+ isLoading: true,
+};
+
+export const NoData = Template.bind({});
+NoData.args = {
+ ...defaultArgs,
+ chartData: [],
+};
+
+export const WithChartInDefaultSlot = WithScatterplot.bind({});
+WithChartInDefaultSlot.args = {
+ ...defaultArgs,
+ isBlob: true,
+ scatterplotMainData,
+};
diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_column.stories.js b/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_column.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..850b4dba335831b83ed00ef79d4a40fdd5b6dd52
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/components/metric_column.stories.js
@@ -0,0 +1,36 @@
+import MetricColumn from './metric_column.vue';
+
+export default {
+ component: MetricColumn,
+ title: 'ee/analytics/productivity_analytics/components/metric_column',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MetricColumn },
+ props: Object.keys(argTypes),
+ template: `
+ `,
+});
+
+const defaultArgs = {
+ type: 'days_to_merge',
+ value: 10,
+ label: 'Fake metric column',
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ ...defaultArgs,
+};
+
+export const NoValue = Template.bind({});
+NoValue.args = {
+ ...defaultArgs,
+ value: null,
+};
+
+export const NoLabel = Template.bind({});
+NoLabel.args = {
+ ...defaultArgs,
+ label: '',
+};
diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table.stories.js b/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..f207b3695bc72b879734a6dd06142d9b1087fcdc
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table.stories.js
@@ -0,0 +1,50 @@
+import MergeRequestTable from './mr_table.vue';
+import { mergeRequests, metricTypes, metricType, metricLabel } from './stories_constants';
+
+// Note: Some custom styling is missing in the storybook bundle
+// from ee/app/assets/stylesheets/page_bundles/productivity_analytics.scss
+// We should review the CSS to see what can be replaced with util classes
+// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/490201
+
+export default {
+ component: MergeRequestTable,
+ title: 'ee/analytics/productivity_analytics/components/mr_table',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MergeRequestTable },
+ props: Object.keys(argTypes),
+ template: `
+ `,
+});
+
+const defaultArgs = {
+ mergeRequests,
+ pageInfo: {
+ perPage: 2,
+ page: 1,
+ total: 4,
+ totalPages: 2,
+ nextPage: null,
+ previousPage: null,
+ },
+ columnOptions: metricTypes,
+ metricType,
+ metricLabel,
+};
+
+export const Default = Template.bind({});
+Default.args = defaultArgs;
+
+export const NoPagination = Template.bind({});
+NoPagination.args = {
+ ...defaultArgs,
+ pageInfo: {},
+};
+
+export const NoMergeRequests = Template.bind({});
+NoMergeRequests.args = {
+ ...defaultArgs,
+ pageInfo: {},
+ mergeRequests: [],
+};
diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table_row.stories.js b/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table_row.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..364845be0c68c92b9273e4704c61e30daac1e82f
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/components/mr_table_row.stories.js
@@ -0,0 +1,27 @@
+import MergeRequestTableRow from './mr_table_row.vue';
+import { mergeRequests } from './stories_constants';
+
+// Note: Some custom styling is missing in the storybook bundle
+// from ee/app/assets/stylesheets/page_bundles/productivity_analytics.scss
+// We should review the CSS to see what can be replaced with util classes
+// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/490201
+
+export default {
+ component: MergeRequestTableRow,
+ title: 'ee/analytics/productivity_analytics/components/mr_table_row',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MergeRequestTableRow },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const defaultArgs = {
+ mergeRequest: mergeRequests[0],
+ metricType: 'days_to_merge',
+ metricLabel: 'Days to merge',
+};
+
+export const Default = Template.bind({});
+Default.args = defaultArgs;
diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/components/stories_constants.js b/ee/app/assets/javascripts/analytics/productivity_analytics/components/stories_constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b245da09ee395187ab1844f4d0dfcf5af408e5e
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/components/stories_constants.js
@@ -0,0 +1,56 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+export const mergeRequests = [
+ {
+ id: 285,
+ iid: 15,
+ description: 'Fixes #15',
+ title: 'Productivity Analytics merge_request',
+ days_to_merge: 24,
+ time_to_first_comment: 173,
+ time_to_last_commit: 162,
+ time_to_merge: 249,
+ commits_count: 1,
+ loc_per_commit: 3,
+ files_touched: 1,
+ author_avatar_url:
+ 'https://www.gravatar.com/avatar/d3e05dc6a5abad23d24bf2b1b9737b7cc2871d87b8ce5aebe1b7a8201fd3684d?s=80&d=identicon',
+ merge_request_url: 'http://gdk.test:3001/yaml-config/project-with-data/-/merge_requests/15',
+ },
+ {
+ id: 276,
+ iid: 6,
+ description: 'Fixes #6',
+ title: 'Productivity Analytics merge_request',
+ days_to_merge: 13,
+ time_to_first_comment: 199,
+ time_to_last_commit: -343,
+ time_to_merge: 465,
+ commits_count: 1,
+ loc_per_commit: 3,
+ files_touched: 1,
+ author_avatar_url:
+ 'https://www.gravatar.com/avatar/bca13b210f8df07736342b3124da2f55619bca35fbfa6e9165cce4a50fa629a9?s=80&d=identicon',
+ merge_request_url: 'http://gdk.test:3001/yaml-config/project-with-data/-/merge_requests/6',
+ },
+];
+
+export const metricType = 'time_to_first_comment';
+export const metricLabel = 'Time from first commit until first comment';
+
+export const metricTypes = [
+ {
+ key: metricType,
+ label: metricLabel,
+ charts: ['timeBasedHistogram', 'scatterplot'],
+ },
+ {
+ key: 'time_to_last_commit',
+ label: 'Time from first comment to last commit',
+ charts: ['timeBasedHistogram', 'scatterplot'],
+ },
+ {
+ key: 'time_to_merge',
+ label: 'Time from last commit to merge',
+ charts: ['timeBasedHistogram', 'scatterplot'],
+ },
+];