@axis-backstage/plugin-jira-dashboard-backend 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/api.cjs.js +129 -0
- package/dist/api.cjs.js.map +1 -0
- package/dist/config.cjs.js +126 -0
- package/dist/config.cjs.js.map +1 -0
- package/dist/filters.cjs.js +39 -0
- package/dist/filters.cjs.js.map +1 -0
- package/dist/index.cjs.js +9 -740
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib.cjs.js +38 -0
- package/dist/lib.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +53 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/queries.cjs.js +30 -0
- package/dist/queries.cjs.js.map +1 -0
- package/dist/service/defaultValues.cjs.js +6 -0
- package/dist/service/defaultValues.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +255 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/service/service.cjs.js +136 -0
- package/dist/service/service.cjs.js.map +1 -0
- package/package.json +22 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @axis-backstage/plugin-jira-dashboard-backend
|
|
2
2
|
|
|
3
|
+
## 4.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ede8341: Updated to Backstage v1.36.1.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [ede8341]
|
|
12
|
+
- @axis-backstage/plugin-jira-dashboard-common@1.10.0
|
|
13
|
+
|
|
14
|
+
## 4.3.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 22754be: Remove unused `@backstage/backend-common` package dependency.
|
|
19
|
+
|
|
3
20
|
## 4.3.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/dist/api.cjs.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fetch = require('node-fetch');
|
|
4
|
+
var queries = require('./queries.cjs.js');
|
|
5
|
+
var errors = require('@backstage/errors');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
10
|
+
|
|
11
|
+
const getProjectInfo = async (project) => {
|
|
12
|
+
const { projectKey, instance } = project;
|
|
13
|
+
const response = await callApi(
|
|
14
|
+
instance,
|
|
15
|
+
`${instance.baseUrl}project/${projectKey}`,
|
|
16
|
+
{
|
|
17
|
+
method: "GET",
|
|
18
|
+
headers: {
|
|
19
|
+
Accept: "application/json"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
if (response.status !== 200) {
|
|
24
|
+
throw Error(
|
|
25
|
+
`Request failed with status code ${response.status}: ${response.statusText}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
};
|
|
30
|
+
const getFilterById = async (id, instance) => {
|
|
31
|
+
const response = await callApi(instance, `${instance.baseUrl}filter/${id}`, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: "application/json"
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (response.status !== 200) {
|
|
38
|
+
throw Error(`${response.status}`);
|
|
39
|
+
}
|
|
40
|
+
const jsonResponse = await response.json();
|
|
41
|
+
return { name: jsonResponse.name, query: jsonResponse.jql };
|
|
42
|
+
};
|
|
43
|
+
const getIssuesByFilter = async (projects, components, query) => {
|
|
44
|
+
const issues = [];
|
|
45
|
+
for (const project of projects) {
|
|
46
|
+
const { projectKey, instance } = project;
|
|
47
|
+
const jql = queries.jqlQueryBuilder({ project: [projectKey], components, query });
|
|
48
|
+
const response = await callApi(
|
|
49
|
+
instance,
|
|
50
|
+
`${instance.baseUrl}search?jql=${jql}`,
|
|
51
|
+
{
|
|
52
|
+
method: "GET",
|
|
53
|
+
headers: {
|
|
54
|
+
Accept: "application/json"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
).then((resp) => resp.json()).catch(() => null);
|
|
58
|
+
if (response?.issues) {
|
|
59
|
+
issues.push(...response.issues);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return issues;
|
|
63
|
+
};
|
|
64
|
+
const searchJira = async (instance, jqlQuery, options) => {
|
|
65
|
+
const response = await callApi(instance, `${instance.baseUrl}search`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify({ jql: jqlQuery, ...options }),
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
"Content-Type": "application/json"
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw await errors.ResponseError.fromResponse(response);
|
|
75
|
+
}
|
|
76
|
+
const jsonResponse = await response.json();
|
|
77
|
+
return jsonResponse;
|
|
78
|
+
};
|
|
79
|
+
const getIssuesByComponent = async (projects, componentKeys) => {
|
|
80
|
+
if (projects.length === 0) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const projectKeys = projects.map((project) => project.projectKey).join(",");
|
|
84
|
+
const components = componentKeys.split(",").map((component) => `'${component.trim()}'`).join(",");
|
|
85
|
+
const jql = `project in (${projectKeys}) AND component in (${components})`;
|
|
86
|
+
const { instance } = projects[0];
|
|
87
|
+
try {
|
|
88
|
+
const response = await callApi(
|
|
89
|
+
instance,
|
|
90
|
+
`${instance.baseUrl}search?jql=${jql}`,
|
|
91
|
+
{
|
|
92
|
+
method: "GET",
|
|
93
|
+
headers: {
|
|
94
|
+
Accept: "application/json"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
).then((resp) => resp.json());
|
|
98
|
+
if (!response.issues || response.issues.length === 0) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return response.issues;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.message.includes("does not exist for the field 'project'")) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
async function getProjectAvatar(url, instance) {
|
|
110
|
+
return callApi(instance, url);
|
|
111
|
+
}
|
|
112
|
+
async function callApi(instance, url, init) {
|
|
113
|
+
const requestInit = init ?? { method: "GET" };
|
|
114
|
+
requestInit.headers = {
|
|
115
|
+
...instance.headers,
|
|
116
|
+
Authorization: instance.token,
|
|
117
|
+
...requestInit.headers
|
|
118
|
+
};
|
|
119
|
+
return fetch__default.default(url, requestInit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
exports.callApi = callApi;
|
|
123
|
+
exports.getFilterById = getFilterById;
|
|
124
|
+
exports.getIssuesByComponent = getIssuesByComponent;
|
|
125
|
+
exports.getIssuesByFilter = getIssuesByFilter;
|
|
126
|
+
exports.getProjectAvatar = getProjectAvatar;
|
|
127
|
+
exports.getProjectInfo = getProjectInfo;
|
|
128
|
+
exports.searchJira = searchJira;
|
|
129
|
+
//# sourceMappingURL=api.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.cjs.js","sources":["../src/api.ts"],"sourcesContent":["import fetch, { RequestInit } from 'node-fetch';\nimport {\n Filter,\n Issue,\n Project,\n JiraQueryResults,\n} from '@axis-backstage/plugin-jira-dashboard-common';\n\nimport type { ConfigInstance } from './config';\nimport { jqlQueryBuilder } from './queries';\nimport type { JiraProject } from './lib';\nimport { ResponseError } from '@backstage/errors';\n\nexport const getProjectInfo = async (\n project: JiraProject,\n): Promise<Project> => {\n const { projectKey, instance } = project;\n const response = await callApi(\n instance,\n `${instance.baseUrl}project/${projectKey}`,\n {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n },\n },\n );\n if (response.status !== 200) {\n throw Error(\n `Request failed with status code ${response.status}: ${response.statusText}`,\n );\n }\n return response.json();\n};\n\nexport const getFilterById = async (\n id: string,\n instance: ConfigInstance,\n): Promise<Filter> => {\n const response = await callApi(instance, `${instance.baseUrl}filter/${id}`, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n },\n });\n if (response.status !== 200) {\n throw Error(`${response.status}`);\n }\n const jsonResponse = await response.json();\n return { name: jsonResponse.name, query: jsonResponse.jql } as Filter;\n};\n\nexport const getIssuesByFilter = async (\n projects: JiraProject[],\n components: string[],\n query: string,\n): Promise<Issue[]> => {\n const issues: Issue[] = [];\n for (const project of projects) {\n const { projectKey, instance } = project;\n const jql = jqlQueryBuilder({ project: [projectKey], components, query });\n const response = await callApi(\n instance,\n `${instance.baseUrl}search?jql=${jql}`,\n {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n },\n },\n )\n .then(resp => resp.json())\n .catch(() => null);\n\n if (response?.issues) {\n issues.push(...response.issues);\n }\n }\n\n return issues;\n};\n\n/**\n * Options available for the Jira JQL query.\n *\n * @public\n */\nexport type SearchOptions = {\n expand?: string[];\n fields?: string[];\n fieldsByKey?: boolean;\n properties?: string[];\n startAt?: number;\n maxResults?: number;\n validateQuery?: string;\n};\n\n/**\n * Asynchronously searches for Jira issues using JQL.\n *\n * For more information about the available options, see the API documentation at:\n * https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-post\n *\n * @param config - A Backstage config\n * @param jqlQuery - A string containing the jql query.\n * @param options - Query options that will be passed on to the POST request.\n * @public\n */\nexport const searchJira = async (\n instance: ConfigInstance,\n jqlQuery: string,\n options: SearchOptions,\n): Promise<JiraQueryResults> => {\n const response = await callApi(instance, `${instance.baseUrl}search`, {\n method: 'POST',\n body: JSON.stringify({ jql: jqlQuery, ...options }),\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n });\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n const jsonResponse = await response.json();\n return jsonResponse as JiraQueryResults;\n};\n\nexport const getIssuesByComponent = async (\n projects: JiraProject[],\n componentKeys: string,\n): Promise<Issue[]> => {\n // Return an empty array if no projects are provided\n if (projects.length === 0) {\n return [];\n }\n\n const projectKeys = projects.map(project => project.projectKey).join(',');\n const components = componentKeys\n .split(',')\n .map(component => `'${component.trim()}'`)\n .join(',');\n\n const jql = `project in (${projectKeys}) AND component in (${components})`;\n const { instance } = projects[0];\n\n try {\n const response = await callApi(\n instance,\n `${instance.baseUrl}search?jql=${jql}`,\n {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n },\n },\n ).then(resp => resp.json());\n\n if (!response.issues || response.issues.length === 0) {\n return [];\n }\n\n return response.issues;\n } catch (error: any) {\n if (error.message.includes(\"does not exist for the field 'project'\")) {\n return [];\n }\n throw error;\n }\n};\nexport async function getProjectAvatar(url: string, instance: ConfigInstance) {\n return callApi(instance, url);\n}\n\n/**\n * Call the Jira API using fetch.\n *\n * This function injects the auth token and custom headers.\n *\n * @public\n */\nexport async function callApi(\n instance: ConfigInstance,\n url: string,\n init?: RequestInit,\n) {\n const requestInit = init ?? { method: 'GET' };\n\n // Inject custom headers from config, Authorization token and headers from the\n // request\n requestInit.headers = {\n ...instance.headers,\n Authorization: instance.token,\n ...requestInit.headers,\n };\n return fetch(url, requestInit);\n}\n"],"names":["jqlQueryBuilder","ResponseError","fetch"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
|
|
5
|
+
function parseHeaders(config) {
|
|
6
|
+
if (!config) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
config.keys().map((key) => {
|
|
11
|
+
const value = config.getString(key);
|
|
12
|
+
return [key, value];
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const JIRA_CONFIG_BASE_URL = "baseUrl";
|
|
17
|
+
const JIRA_CONFIG_TOKEN = "token";
|
|
18
|
+
const JIRA_CONFIG_HEADERS = "headers";
|
|
19
|
+
const JIRA_CONFIG_USER_EMAIL_SUFFIX = "userEmailSuffix";
|
|
20
|
+
const JIRA_CONFIG_ANNOTATION = "annotationPrefix";
|
|
21
|
+
const JIRA_FILTERS = "defaultFilters";
|
|
22
|
+
class JiraConfig {
|
|
23
|
+
instances = {};
|
|
24
|
+
/**
|
|
25
|
+
* The annotation prefix to use for Jira annotations
|
|
26
|
+
*/
|
|
27
|
+
annotationPrefix;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
const jira = config.getConfig("jiraDashboard");
|
|
30
|
+
this.annotationPrefix = jira.getOptionalString(JIRA_CONFIG_ANNOTATION) ?? "jira.com";
|
|
31
|
+
const instances = jira.getOptionalConfigArray("instances");
|
|
32
|
+
if (instances) {
|
|
33
|
+
instances.forEach((inst) => {
|
|
34
|
+
const name = inst.getString("name");
|
|
35
|
+
if (Object.getOwnPropertyNames(this.instances).includes(name)) {
|
|
36
|
+
throw new errors.ConflictError(
|
|
37
|
+
`Duplicate jiraDashboard instances: '${name}'`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.instances[name] = {
|
|
41
|
+
token: inst.getString(JIRA_CONFIG_TOKEN),
|
|
42
|
+
headers: parseHeaders(inst.getOptionalConfig(JIRA_CONFIG_HEADERS)),
|
|
43
|
+
baseUrl: inst.getString(JIRA_CONFIG_BASE_URL),
|
|
44
|
+
userEmailSuffix: inst.getOptionalString(
|
|
45
|
+
JIRA_CONFIG_USER_EMAIL_SUFFIX
|
|
46
|
+
),
|
|
47
|
+
defaultFilters: inst.getOptionalConfigArray(JIRA_FILTERS)?.map((filterConfig) => ({
|
|
48
|
+
name: filterConfig.getString("name"),
|
|
49
|
+
shortName: filterConfig.getString("shortName"),
|
|
50
|
+
query: filterConfig.getString("query")
|
|
51
|
+
}))
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
this.instances.default = {
|
|
56
|
+
token: jira.getString(JIRA_CONFIG_TOKEN),
|
|
57
|
+
headers: parseHeaders(jira.getOptionalConfig(JIRA_CONFIG_HEADERS)),
|
|
58
|
+
baseUrl: jira.getString(JIRA_CONFIG_BASE_URL),
|
|
59
|
+
userEmailSuffix: jira.getOptionalString(JIRA_CONFIG_USER_EMAIL_SUFFIX),
|
|
60
|
+
defaultFilters: jira.getOptionalConfigArray(JIRA_FILTERS)?.map((filterConfig) => ({
|
|
61
|
+
name: filterConfig.getString("name"),
|
|
62
|
+
shortName: filterConfig.getString("shortName"),
|
|
63
|
+
query: filterConfig.getString("query")
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a JiraConfig from the root config
|
|
70
|
+
*/
|
|
71
|
+
static fromConfig(config) {
|
|
72
|
+
return new JiraConfig(config);
|
|
73
|
+
}
|
|
74
|
+
forInstance(instanceName) {
|
|
75
|
+
const instance = this.instances[instanceName];
|
|
76
|
+
if (!instance) {
|
|
77
|
+
throw new errors.ServiceUnavailableError(
|
|
78
|
+
`No such jira instance '${instanceName}'`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return instance;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns the configuration all instances
|
|
85
|
+
*/
|
|
86
|
+
getInstances() {
|
|
87
|
+
return Object.getOwnPropertyNames(this.instances);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the jira config for a specific instance
|
|
91
|
+
*/
|
|
92
|
+
getInstance(instanceName) {
|
|
93
|
+
return this.forInstance(instanceName ?? "default");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the jira base url for a given instance
|
|
97
|
+
*/
|
|
98
|
+
resolveJiraBaseUrl(instanceName) {
|
|
99
|
+
const instance = this.forInstance(instanceName);
|
|
100
|
+
return instance.baseUrl;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get the auth token for a given instance
|
|
104
|
+
*/
|
|
105
|
+
resolveJiraToken(instanceName) {
|
|
106
|
+
const instance = this.forInstance(instanceName);
|
|
107
|
+
return instance.token;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the email suffice for a given instance
|
|
111
|
+
*/
|
|
112
|
+
resolveUserEmailSuffix(instanceName) {
|
|
113
|
+
const instance = this.forInstance(instanceName);
|
|
114
|
+
return instance.userEmailSuffix;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the defined default filters for a given instance
|
|
118
|
+
*/
|
|
119
|
+
resolveDefaultFilters(instanceName) {
|
|
120
|
+
const instance = this.forInstance(instanceName);
|
|
121
|
+
return instance.defaultFilters;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
exports.JiraConfig = JiraConfig;
|
|
126
|
+
//# sourceMappingURL=config.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.cjs.js","sources":["../src/config.ts"],"sourcesContent":["import { ConflictError, ServiceUnavailableError } from '@backstage/errors';\nimport { RootConfigService } from '@backstage/backend-plugin-api';\nimport { Filter } from '@axis-backstage/plugin-jira-dashboard-common';\n\ntype Config = ReturnType<RootConfigService['getConfig']>;\n\nfunction parseHeaders(config: Config | undefined): Record<string, string> {\n if (!config) {\n return {};\n }\n return Object.fromEntries(\n config.keys().map(key => {\n const value = config.getString(key);\n return [key, value] as const;\n }),\n );\n}\n\n/**\n * The configuration of a Jira instance\n *\n * @public\n */\nexport type ConfigInstance = {\n token: string;\n headers: Record<string, string>;\n baseUrl: string;\n userEmailSuffix?: string;\n defaultFilters?: Filter[];\n};\n\nconst JIRA_CONFIG_BASE_URL = 'baseUrl';\nconst JIRA_CONFIG_TOKEN = 'token';\nconst JIRA_CONFIG_HEADERS = 'headers';\nconst JIRA_CONFIG_USER_EMAIL_SUFFIX = 'userEmailSuffix';\nconst JIRA_CONFIG_ANNOTATION = 'annotationPrefix';\nconst JIRA_FILTERS = 'defaultFilters';\n\n/**\n * Class for reading Jira configuration from the root config\n *\n * @public\n */\nexport class JiraConfig {\n private instances: Record<string, ConfigInstance> = {};\n /**\n * The annotation prefix to use for Jira annotations\n */\n public readonly annotationPrefix: string;\n\n private constructor(config: RootConfigService) {\n const jira = config.getConfig('jiraDashboard');\n\n this.annotationPrefix =\n jira.getOptionalString(JIRA_CONFIG_ANNOTATION) ?? 'jira.com';\n\n const instances = jira.getOptionalConfigArray('instances');\n if (instances) {\n // Multiple instances form\n instances.forEach(inst => {\n const name = inst.getString('name');\n if (Object.getOwnPropertyNames(this.instances).includes(name)) {\n throw new ConflictError(\n `Duplicate jiraDashboard instances: '${name}'`,\n );\n }\n\n this.instances[name] = {\n token: inst.getString(JIRA_CONFIG_TOKEN),\n headers: parseHeaders(inst.getOptionalConfig(JIRA_CONFIG_HEADERS)),\n baseUrl: inst.getString(JIRA_CONFIG_BASE_URL),\n userEmailSuffix: inst.getOptionalString(\n JIRA_CONFIG_USER_EMAIL_SUFFIX,\n ),\n defaultFilters: inst\n .getOptionalConfigArray(JIRA_FILTERS)\n ?.map(filterConfig => ({\n name: filterConfig.getString('name'),\n shortName: filterConfig.getString('shortName'),\n query: filterConfig.getString('query'),\n })),\n };\n });\n } else {\n // Default form\n this.instances.default = {\n token: jira.getString(JIRA_CONFIG_TOKEN),\n headers: parseHeaders(jira.getOptionalConfig(JIRA_CONFIG_HEADERS)),\n baseUrl: jira.getString(JIRA_CONFIG_BASE_URL),\n userEmailSuffix: jira.getOptionalString(JIRA_CONFIG_USER_EMAIL_SUFFIX),\n defaultFilters: jira\n .getOptionalConfigArray(JIRA_FILTERS)\n ?.map(filterConfig => ({\n name: filterConfig.getString('name'),\n shortName: filterConfig.getString('shortName'),\n query: filterConfig.getString('query'),\n })),\n };\n }\n }\n\n /**\n * Create a JiraConfig from the root config\n */\n public static fromConfig(config: RootConfigService): JiraConfig {\n return new JiraConfig(config);\n }\n\n private forInstance(instanceName: string) {\n const instance = this.instances[instanceName];\n if (!instance) {\n throw new ServiceUnavailableError(\n `No such jira instance '${instanceName}'`,\n );\n }\n return instance;\n }\n\n /**\n * Returns the configuration all instances\n */\n getInstances() {\n return Object.getOwnPropertyNames(this.instances);\n }\n\n /**\n * Get the jira config for a specific instance\n */\n getInstance(instanceName?: string): ConfigInstance {\n return this.forInstance(instanceName ?? 'default');\n }\n\n /**\n * Get the jira base url for a given instance\n */\n resolveJiraBaseUrl(instanceName: string): string {\n const instance = this.forInstance(instanceName);\n return instance.baseUrl;\n }\n\n /**\n * Get the auth token for a given instance\n */\n resolveJiraToken(instanceName: string): string {\n const instance = this.forInstance(instanceName);\n return instance.token;\n }\n\n /**\n * Get the email suffice for a given instance\n */\n resolveUserEmailSuffix(instanceName: string): string | undefined {\n const instance = this.forInstance(instanceName);\n return instance.userEmailSuffix;\n }\n\n /**\n * Get the defined default filters for a given instance\n */\n resolveDefaultFilters(instanceName: string): Filter[] | undefined {\n const instance = this.forInstance(instanceName);\n return instance.defaultFilters;\n }\n}\n"],"names":["ConflictError","ServiceUnavailableError"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const openFilter = {
|
|
4
|
+
name: "Open Issues",
|
|
5
|
+
shortName: "OPEN",
|
|
6
|
+
query: "resolution = Unresolved ORDER BY updated DESC"
|
|
7
|
+
};
|
|
8
|
+
const getIncomingFilter = (incomingStatus) => ({
|
|
9
|
+
name: "Incoming Issues",
|
|
10
|
+
shortName: "INCOMING",
|
|
11
|
+
query: `status = '${incomingStatus}' ORDER BY created ASC`
|
|
12
|
+
});
|
|
13
|
+
const getAssigneUser = (instance, userEntity) => {
|
|
14
|
+
const emailSuffixConfig = instance.userEmailSuffix;
|
|
15
|
+
return emailSuffixConfig ? `${userEntity.metadata.name}${emailSuffixConfig}` : userEntity.spec?.profile?.email || userEntity.metadata.name;
|
|
16
|
+
};
|
|
17
|
+
const getAssignedToMeFilter = (userEntity, instance) => {
|
|
18
|
+
const email = getAssigneUser(instance, userEntity);
|
|
19
|
+
return {
|
|
20
|
+
name: "Assigned to me",
|
|
21
|
+
shortName: "ME",
|
|
22
|
+
query: `assignee = "${email}" AND resolution = Unresolved ORDER BY updated DESC`
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const getDefaultFiltersForUser = (instance, userEntity, incomingStatus) => {
|
|
26
|
+
const incomingFilter = getIncomingFilter(incomingStatus ?? "New");
|
|
27
|
+
const defaultFilters = instance.defaultFilters?.map((filter) => ({
|
|
28
|
+
name: filter.name,
|
|
29
|
+
query: filter.query,
|
|
30
|
+
shortName: filter.shortName
|
|
31
|
+
})) || [];
|
|
32
|
+
if (!userEntity) return [openFilter, incomingFilter, ...defaultFilters];
|
|
33
|
+
const assigneeToMeFilter = getAssignedToMeFilter(userEntity, instance);
|
|
34
|
+
return [openFilter, incomingFilter, assigneeToMeFilter, ...defaultFilters];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.getAssigneUser = getAssigneUser;
|
|
38
|
+
exports.getDefaultFiltersForUser = getDefaultFiltersForUser;
|
|
39
|
+
//# sourceMappingURL=filters.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.cjs.js","sources":["../src/filters.ts"],"sourcesContent":["import { Filter } from '@axis-backstage/plugin-jira-dashboard-common';\nimport { UserEntity } from '@backstage/catalog-model';\nimport { ConfigInstance } from './config';\n\nconst openFilter: Filter = {\n name: 'Open Issues',\n shortName: 'OPEN',\n query: 'resolution = Unresolved ORDER BY updated DESC',\n};\n\nconst getIncomingFilter = (incomingStatus: string): Filter => ({\n name: 'Incoming Issues',\n shortName: 'INCOMING',\n query: `status = '${incomingStatus}' ORDER BY created ASC`,\n});\n\n/**\n * Get the username value for Jira assignee from the user entity.\n * If the email suffix is defined, the result combines `metadata.name` with the email suffix - otherwise if the user entity has a profile email or the metadata name is used.\n * Otherwise, the user entity name will be used.\n * @param config appConfig configuration\n * @param userEntity user entity instance\n */\nexport const getAssigneUser = (\n instance: ConfigInstance,\n userEntity: UserEntity,\n): string => {\n const emailSuffixConfig = instance.userEmailSuffix;\n\n return emailSuffixConfig\n ? `${userEntity.metadata.name}${emailSuffixConfig}`\n : userEntity.spec?.profile?.email || userEntity.metadata.name;\n};\n\nconst getAssignedToMeFilter = (\n userEntity: UserEntity,\n instance: ConfigInstance,\n): Filter => {\n const email = getAssigneUser(instance, userEntity);\n\n return {\n name: 'Assigned to me',\n shortName: 'ME',\n query: `assignee = \"${email}\" AND resolution = Unresolved ORDER BY updated DESC`,\n };\n};\n\nexport const getDefaultFiltersForUser = (\n instance: ConfigInstance,\n userEntity?: UserEntity,\n incomingStatus?: string,\n): Filter[] => {\n const incomingFilter = getIncomingFilter(incomingStatus ?? 'New');\n\n const defaultFilters =\n instance.defaultFilters?.map(filter => ({\n name: filter.name,\n query: filter.query,\n shortName: filter.shortName,\n })) || [];\n\n if (!userEntity) return [openFilter, incomingFilter, ...defaultFilters];\n\n const assigneeToMeFilter = getAssignedToMeFilter(userEntity, instance);\n\n return [openFilter, incomingFilter, assigneeToMeFilter, ...defaultFilters];\n};\n"],"names":[],"mappings":";;AAIA,MAAM,UAAqB,GAAA;AAAA,EACzB,IAAM,EAAA,aAAA;AAAA,EACN,SAAW,EAAA,MAAA;AAAA,EACX,KAAO,EAAA;AACT,CAAA;AAEA,MAAM,iBAAA,GAAoB,CAAC,cAAoC,MAAA;AAAA,EAC7D,IAAM,EAAA,iBAAA;AAAA,EACN,SAAW,EAAA,UAAA;AAAA,EACX,KAAA,EAAO,aAAa,cAAc,CAAA,sBAAA;AACpC,CAAA,CAAA;AASa,MAAA,cAAA,GAAiB,CAC5B,QAAA,EACA,UACW,KAAA;AACX,EAAA,MAAM,oBAAoB,QAAS,CAAA,eAAA;AAEnC,EAAA,OAAO,iBACH,GAAA,CAAA,EAAG,UAAW,CAAA,QAAA,CAAS,IAAI,CAAA,EAAG,iBAAiB,CAAA,CAAA,GAC/C,UAAW,CAAA,IAAA,EAAM,OAAS,EAAA,KAAA,IAAS,WAAW,QAAS,CAAA,IAAA;AAC7D;AAEA,MAAM,qBAAA,GAAwB,CAC5B,UAAA,EACA,QACW,KAAA;AACX,EAAM,MAAA,KAAA,GAAQ,cAAe,CAAA,QAAA,EAAU,UAAU,CAAA;AAEjD,EAAO,OAAA;AAAA,IACL,IAAM,EAAA,gBAAA;AAAA,IACN,SAAW,EAAA,IAAA;AAAA,IACX,KAAA,EAAO,eAAe,KAAK,CAAA,mDAAA;AAAA,GAC7B;AACF,CAAA;AAEO,MAAM,wBAA2B,GAAA,CACtC,QACA,EAAA,UAAA,EACA,cACa,KAAA;AACb,EAAM,MAAA,cAAA,GAAiB,iBAAkB,CAAA,cAAA,IAAkB,KAAK,CAAA;AAEhE,EAAA,MAAM,cACJ,GAAA,QAAA,CAAS,cAAgB,EAAA,GAAA,CAAI,CAAW,MAAA,MAAA;AAAA,IACtC,MAAM,MAAO,CAAA,IAAA;AAAA,IACb,OAAO,MAAO,CAAA,KAAA;AAAA,IACd,WAAW,MAAO,CAAA;AAAA,GACpB,CAAE,KAAK,EAAC;AAEV,EAAA,IAAI,CAAC,UAAY,EAAA,OAAO,CAAC,UAAY,EAAA,cAAA,EAAgB,GAAG,cAAc,CAAA;AAEtE,EAAM,MAAA,kBAAA,GAAqB,qBAAsB,CAAA,UAAA,EAAY,QAAQ,CAAA;AAErE,EAAA,OAAO,CAAC,UAAA,EAAY,cAAgB,EAAA,kBAAA,EAAoB,GAAG,cAAc,CAAA;AAC3E;;;;;"}
|