mihari 5.2.3 → 5.2.4
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.
- checksums.yaml +4 -4
- data/build_frontend.sh +1 -9
- data/frontend/.browserslistrc +3 -0
- data/frontend/.eslintrc.js +33 -0
- data/frontend/.gitignore +25 -0
- data/frontend/README.md +3 -0
- data/frontend/babel.config.js +3 -0
- data/frontend/index.html +21 -0
- data/frontend/jest.config.js +9 -0
- data/frontend/package-lock.json +13216 -0
- data/frontend/package.json +54 -0
- data/frontend/public/favicon.ico +0 -0
- data/frontend/scripts/swagger_doc_to_yaml.rb +23 -0
- data/frontend/src/App.vue +27 -0
- data/frontend/src/api-helper.ts +113 -0
- data/frontend/src/api.ts +105 -0
- data/frontend/src/components/ErrorMessage.vue +32 -0
- data/frontend/src/components/Loading.vue +15 -0
- data/frontend/src/components/Navbar.vue +59 -0
- data/frontend/src/components/Pagination.vue +126 -0
- data/frontend/src/components/alert/Alert.vue +92 -0
- data/frontend/src/components/alert/Alerts.vue +66 -0
- data/frontend/src/components/alert/AlertsWithPagination.vue +91 -0
- data/frontend/src/components/alert/AlertsWrapper.vue +141 -0
- data/frontend/src/components/alert/Form.vue +185 -0
- data/frontend/src/components/artifact/AS.vue +29 -0
- data/frontend/src/components/artifact/Artifact.vue +321 -0
- data/frontend/src/components/artifact/ArtifactTag.vue +70 -0
- data/frontend/src/components/artifact/ArtifactTags.vue +29 -0
- data/frontend/src/components/artifact/ArtifactWrapper.vue +62 -0
- data/frontend/src/components/artifact/CPEs.vue +23 -0
- data/frontend/src/components/artifact/DnsRecords.vue +38 -0
- data/frontend/src/components/artifact/Ports.vue +23 -0
- data/frontend/src/components/artifact/ReverseDnsNames.vue +31 -0
- data/frontend/src/components/artifact/Tags.vue +29 -0
- data/frontend/src/components/artifact/WhoisRecord.vue +49 -0
- data/frontend/src/components/config/Configs.vue +68 -0
- data/frontend/src/components/config/ConfigsWrapper.vue +40 -0
- data/frontend/src/components/link/Link.vue +32 -0
- data/frontend/src/components/link/Links.vue +47 -0
- data/frontend/src/components/rule/EditRule.vue +74 -0
- data/frontend/src/components/rule/EditRuleWrapper.vue +56 -0
- data/frontend/src/components/rule/Form.vue +160 -0
- data/frontend/src/components/rule/InputForm.vue +80 -0
- data/frontend/src/components/rule/NewRule.vue +60 -0
- data/frontend/src/components/rule/Rule.vue +108 -0
- data/frontend/src/components/rule/RuleWrapper.vue +62 -0
- data/frontend/src/components/rule/Rules.vue +88 -0
- data/frontend/src/components/rule/RulesWrapper.vue +130 -0
- data/frontend/src/components/rule/YAML.vue +47 -0
- data/frontend/src/components/tag/Tag.vue +73 -0
- data/frontend/src/components/tag/Tags.vue +37 -0
- data/frontend/src/countries.ts +350 -0
- data/frontend/src/index.ts +23 -0
- data/frontend/src/links/anyrun.ts +19 -0
- data/frontend/src/links/base.ts +14 -0
- data/frontend/src/links/censys.ts +20 -0
- data/frontend/src/links/crtsh.ts +20 -0
- data/frontend/src/links/dnslytics.ts +38 -0
- data/frontend/src/links/greynoise.ts +20 -0
- data/frontend/src/links/index.ts +40 -0
- data/frontend/src/links/intezer.ts +20 -0
- data/frontend/src/links/otx.ts +33 -0
- data/frontend/src/links/securitytrails.ts +38 -0
- data/frontend/src/links/shodan.ts +20 -0
- data/frontend/src/links/urlscan.ts +50 -0
- data/frontend/src/links/virustotal.ts +72 -0
- data/frontend/src/main.ts +11 -0
- data/frontend/src/router/index.ts +57 -0
- data/frontend/src/rule.ts +14 -0
- data/frontend/src/shims-vue.d.ts +6 -0
- data/frontend/src/swagger.yaml +737 -0
- data/frontend/src/types.ts +188 -0
- data/frontend/src/utils.ts +60 -0
- data/frontend/src/views/Alerts.vue +20 -0
- data/frontend/src/views/Artifact.vue +44 -0
- data/frontend/src/views/Configs.vue +20 -0
- data/frontend/src/views/EditRule.vue +44 -0
- data/frontend/src/views/NewRule.vue +26 -0
- data/frontend/src/views/Rule.vue +44 -0
- data/frontend/src/views/Rules.vue +20 -0
- data/frontend/tests/unit/utils.spec.ts +7 -0
- data/frontend/tsconfig.json +40 -0
- data/frontend/vite.config.js +24 -0
- data/lefthook.yml +10 -0
- data/lib/mihari/analyzers/base.rb +22 -5
- data/lib/mihari/analyzers/rule.rb +8 -29
- data/lib/mihari/commands/search.rb +16 -7
- data/lib/mihari/entities/rule.rb +1 -1
- data/lib/mihari/entities/tag.rb +1 -1
- data/lib/mihari/schemas/analyzer.rb +2 -7
- data/lib/mihari/schemas/rule.rb +1 -1
- data/lib/mihari/structs/config.rb +39 -16
- data/lib/mihari/structs/rule.rb +1 -1
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/public/assets/index-ac4e5ffa.js +50 -0
- data/lib/mihari/web/public/index.html +1 -1
- data/mihari.gemspec +5 -5
- metadata +97 -16
- data/.gitmodules +0 -0
- data/.overcommit.yml +0 -12
- data/lib/mihari/web/public/assets/index-cbe1734c.js +0 -50
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="column">
|
|
3
|
+
<div v-if="runRuleTask.isRunning">
|
|
4
|
+
<Loading></Loading>
|
|
5
|
+
<hr />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div v-if="runRuleTask.last?.error">
|
|
9
|
+
<ErrorMessage :error="runRuleTask.last.error"></ErrorMessage>
|
|
10
|
+
<hr />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<h2 class="is-size-2 mb-4">Rule</h2>
|
|
14
|
+
|
|
15
|
+
<p class="is-clearfix">
|
|
16
|
+
<span class="buttons is-pulled-right">
|
|
17
|
+
<button class="button is-primary is-light is-small" @click="runRule">
|
|
18
|
+
<span>Run</span>
|
|
19
|
+
<span class="icon is-small">
|
|
20
|
+
<i class="fas fa-arrow-right"></i>
|
|
21
|
+
</span>
|
|
22
|
+
</button>
|
|
23
|
+
<router-link
|
|
24
|
+
class="button is-info is-light is-small"
|
|
25
|
+
:to="{ name: 'EditRule', params: { id: rule.id } }"
|
|
26
|
+
>
|
|
27
|
+
<span>Edit</span>
|
|
28
|
+
<span class="icon is-small">
|
|
29
|
+
<i class="fas fa-edit"></i>
|
|
30
|
+
</span>
|
|
31
|
+
</router-link>
|
|
32
|
+
<button class="button is-light is-small" @click="deleteRule">
|
|
33
|
+
<span>Delete</span>
|
|
34
|
+
<span class="icon is-small">
|
|
35
|
+
<i class="fas fa-times"></i>
|
|
36
|
+
</span>
|
|
37
|
+
</button>
|
|
38
|
+
</span>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<YAML :yaml="rule.yaml"></YAML>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<hr />
|
|
45
|
+
|
|
46
|
+
<div class="column">
|
|
47
|
+
<h2 class="is-size-2 mb-4">Related alerts</h2>
|
|
48
|
+
|
|
49
|
+
<Alerts :ruleId="rule.id"></Alerts>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script lang="ts">
|
|
54
|
+
import { defineComponent, PropType } from "vue";
|
|
55
|
+
import { useRouter } from "vue-router";
|
|
56
|
+
|
|
57
|
+
import { generateDeleteRuleTask, generateRunRuleTask } from "@/api-helper";
|
|
58
|
+
import Alerts from "@/components/alert/AlertsWithPagination.vue";
|
|
59
|
+
import ErrorMessage from "@/components/ErrorMessage.vue";
|
|
60
|
+
import Loading from "@/components/Loading.vue";
|
|
61
|
+
import YAML from "@/components/rule/YAML.vue";
|
|
62
|
+
import { Rule } from "@/types";
|
|
63
|
+
|
|
64
|
+
export default defineComponent({
|
|
65
|
+
name: "RuleItem",
|
|
66
|
+
props: {
|
|
67
|
+
rule: {
|
|
68
|
+
type: Object as PropType<Rule>,
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
components: {
|
|
73
|
+
YAML,
|
|
74
|
+
Alerts,
|
|
75
|
+
Loading,
|
|
76
|
+
ErrorMessage,
|
|
77
|
+
},
|
|
78
|
+
emits: ["refresh"],
|
|
79
|
+
setup(props, context) {
|
|
80
|
+
const router = useRouter();
|
|
81
|
+
|
|
82
|
+
const deleteRuleTask = generateDeleteRuleTask();
|
|
83
|
+
const runRuleTask = generateRunRuleTask();
|
|
84
|
+
|
|
85
|
+
const deleteRule = async () => {
|
|
86
|
+
const result = window.confirm(
|
|
87
|
+
`Are you sure you want to delete ${props.rule.id}?`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (result) {
|
|
91
|
+
await deleteRuleTask.perform(props.rule.id);
|
|
92
|
+
router.push("/");
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const runRule = async () => {
|
|
97
|
+
await runRuleTask.perform(props.rule.id);
|
|
98
|
+
context.emit("refresh");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
deleteRule,
|
|
103
|
+
runRule,
|
|
104
|
+
runRuleTask,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Loading v-if="getRuleTask.isRunning"></Loading>
|
|
3
|
+
|
|
4
|
+
<ErrorMessage
|
|
5
|
+
v-if="getRuleTask.isError"
|
|
6
|
+
:error="getRuleTask.last?.error"
|
|
7
|
+
></ErrorMessage>
|
|
8
|
+
|
|
9
|
+
<Rule
|
|
10
|
+
:rule="getRuleTask.last.value"
|
|
11
|
+
@refresh="refresh"
|
|
12
|
+
v-if="getRuleTask.last?.value"
|
|
13
|
+
></Rule>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { defineComponent, onMounted, watch } from "vue";
|
|
18
|
+
|
|
19
|
+
import { generateGetRuleTask } from "@/api-helper";
|
|
20
|
+
import ErrorMessage from "@/components/ErrorMessage.vue";
|
|
21
|
+
import Loading from "@/components/Loading.vue";
|
|
22
|
+
import Rule from "@/components/rule/Rule.vue";
|
|
23
|
+
|
|
24
|
+
export default defineComponent({
|
|
25
|
+
name: "RuleWrapper",
|
|
26
|
+
components: {
|
|
27
|
+
Rule,
|
|
28
|
+
Loading,
|
|
29
|
+
ErrorMessage,
|
|
30
|
+
},
|
|
31
|
+
props: {
|
|
32
|
+
id: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
setup(props) {
|
|
38
|
+
const getRuleTask = generateGetRuleTask();
|
|
39
|
+
|
|
40
|
+
const getRule = async () => {
|
|
41
|
+
await getRuleTask.perform(props.id);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const refresh = async () => {
|
|
45
|
+
await getRule();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
onMounted(async () => {
|
|
49
|
+
await getRule();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
watch(props, async () => {
|
|
53
|
+
await getRule();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
getRuleTask,
|
|
58
|
+
refresh,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="hasRules">
|
|
3
|
+
<table class="table is-fullwidth">
|
|
4
|
+
<tr>
|
|
5
|
+
<th>ID</th>
|
|
6
|
+
<th>Title</th>
|
|
7
|
+
<th>Description</th>
|
|
8
|
+
<th>Tags</th>
|
|
9
|
+
</tr>
|
|
10
|
+
<tr v-for="rule in rules.rules" :key="rule.id">
|
|
11
|
+
<td>
|
|
12
|
+
<router-link :to="{ name: 'Rule', params: { id: rule.id } }">{{
|
|
13
|
+
rule.id
|
|
14
|
+
}}</router-link>
|
|
15
|
+
</td>
|
|
16
|
+
<td>
|
|
17
|
+
{{ rule.title }}
|
|
18
|
+
</td>
|
|
19
|
+
<td>
|
|
20
|
+
{{ rule.description }}
|
|
21
|
+
</td>
|
|
22
|
+
<td>
|
|
23
|
+
<Tags :tags="rule.tags" @update-tag="updateTag"></Tags>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
</table>
|
|
27
|
+
</div>
|
|
28
|
+
<Pagination
|
|
29
|
+
:currentPage="rules.currentPage"
|
|
30
|
+
:total="rules.total"
|
|
31
|
+
:pageSize="rules.pageSize"
|
|
32
|
+
@update-page="updatePage"
|
|
33
|
+
></Pagination>
|
|
34
|
+
<p class="help">
|
|
35
|
+
({{ rules.total }} results in total, {{ rules.rules.length }} shown)
|
|
36
|
+
</p>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { computed, defineComponent, PropType } from "vue";
|
|
41
|
+
|
|
42
|
+
import Pagination from "@/components/Pagination.vue";
|
|
43
|
+
import Tags from "@/components/tag/Tags.vue";
|
|
44
|
+
import { Rules } from "@/types";
|
|
45
|
+
|
|
46
|
+
export default defineComponent({
|
|
47
|
+
name: "RulesItem",
|
|
48
|
+
props: {
|
|
49
|
+
rules: {
|
|
50
|
+
type: Object as PropType<Rules>,
|
|
51
|
+
required: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
components: {
|
|
55
|
+
Pagination,
|
|
56
|
+
Tags,
|
|
57
|
+
},
|
|
58
|
+
emits: ["update-page", "refresh-page", "update-tag"],
|
|
59
|
+
setup(props, context) {
|
|
60
|
+
const scrollToTop = () => {
|
|
61
|
+
window.scrollTo({
|
|
62
|
+
top: 0,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const updatePage = (page: number) => {
|
|
67
|
+
scrollToTop();
|
|
68
|
+
context.emit("update-page", page);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const refreshPage = () => {
|
|
72
|
+
scrollToTop();
|
|
73
|
+
context.emit("refresh-page");
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const updateTag = (tag: string) => {
|
|
77
|
+
scrollToTop();
|
|
78
|
+
context.emit("update-tag", tag);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const hasRules = computed(() => {
|
|
82
|
+
return props.rules.rules.length > 0;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { updatePage, refreshPage, updateTag, hasRules };
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="box mb-6">
|
|
3
|
+
<FormComponent
|
|
4
|
+
ref="form"
|
|
5
|
+
:tags="getTagsTask.last?.value || []"
|
|
6
|
+
:page="page"
|
|
7
|
+
:tag="tag"
|
|
8
|
+
></FormComponent>
|
|
9
|
+
|
|
10
|
+
<hr />
|
|
11
|
+
|
|
12
|
+
<div class="column">
|
|
13
|
+
<div class="field is-grouped is-grouped-centered">
|
|
14
|
+
<p class="control">
|
|
15
|
+
<a class="button is-primary" @click="search">
|
|
16
|
+
<span class="icon is-small">
|
|
17
|
+
<i class="fas fa-search"></i>
|
|
18
|
+
</span>
|
|
19
|
+
<span>Search</span>
|
|
20
|
+
</a>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div v-if="getRulesTask.performCount > 0">
|
|
27
|
+
<hr />
|
|
28
|
+
|
|
29
|
+
<Loading v-if="getRulesTask.isRunning"></Loading>
|
|
30
|
+
|
|
31
|
+
<ErrorMessage
|
|
32
|
+
v-if="getRulesTask.isError"
|
|
33
|
+
:error="getRulesTask.last?.error"
|
|
34
|
+
></ErrorMessage>
|
|
35
|
+
|
|
36
|
+
<Rules
|
|
37
|
+
:rules="getRulesTask.last.value"
|
|
38
|
+
v-if="getRulesTask.last?.value"
|
|
39
|
+
@refresh-page="refreshPage"
|
|
40
|
+
@update-page="updatePage"
|
|
41
|
+
@update-tag="updateTag"
|
|
42
|
+
></Rules>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import { defineComponent, nextTick, onMounted, ref, watch } from "vue";
|
|
48
|
+
|
|
49
|
+
import { generateGetRulesTask, generateGetTagsTask } from "@/api-helper";
|
|
50
|
+
import ErrorMessage from "@/components/ErrorMessage.vue";
|
|
51
|
+
import Loading from "@/components/Loading.vue";
|
|
52
|
+
import FormComponent from "@/components/rule/Form.vue";
|
|
53
|
+
import Rules from "@/components/rule/Rules.vue";
|
|
54
|
+
import { RuleSearchParams } from "@/types";
|
|
55
|
+
|
|
56
|
+
export default defineComponent({
|
|
57
|
+
name: "RulesWrapper",
|
|
58
|
+
components: {
|
|
59
|
+
Rules,
|
|
60
|
+
Loading,
|
|
61
|
+
FormComponent,
|
|
62
|
+
ErrorMessage,
|
|
63
|
+
},
|
|
64
|
+
setup() {
|
|
65
|
+
const page = ref(1);
|
|
66
|
+
const tag = ref<string | undefined>(undefined);
|
|
67
|
+
const form = ref<InstanceType<typeof FormComponent>>();
|
|
68
|
+
|
|
69
|
+
const getRulesTask = generateGetRulesTask();
|
|
70
|
+
const getTagsTask = generateGetTagsTask();
|
|
71
|
+
|
|
72
|
+
const getRules = async () => {
|
|
73
|
+
const params = form.value?.getSearchParams() as RuleSearchParams;
|
|
74
|
+
return await getRulesTask.perform(params);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const updatePage = (newPage: number) => {
|
|
78
|
+
page.value = newPage;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resetPage = () => {
|
|
82
|
+
page.value = 1;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const search = async () => {
|
|
86
|
+
// reset page
|
|
87
|
+
resetPage();
|
|
88
|
+
|
|
89
|
+
await getRules();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const updateTag = (newTag: string | undefined) => {
|
|
93
|
+
if (tag.value === newTag) {
|
|
94
|
+
tag.value = undefined;
|
|
95
|
+
} else {
|
|
96
|
+
tag.value = newTag;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
nextTick(async () => await search());
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const refreshPage = async () => {
|
|
103
|
+
// it is just an alias of search
|
|
104
|
+
// this function will be invoked when a rule is deleted
|
|
105
|
+
await search();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
onMounted(async () => {
|
|
109
|
+
getTagsTask.perform();
|
|
110
|
+
await getRules();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
watch([page, tag], async () => {
|
|
114
|
+
nextTick(async () => await getRules());
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
form,
|
|
119
|
+
getRulesTask,
|
|
120
|
+
getTagsTask,
|
|
121
|
+
page,
|
|
122
|
+
tag,
|
|
123
|
+
refreshPage,
|
|
124
|
+
search,
|
|
125
|
+
updatePage,
|
|
126
|
+
updateTag,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<pre
|
|
3
|
+
ref="pre"
|
|
4
|
+
class="line-numbers"
|
|
5
|
+
><code class="language-yaml">{{ yaml }}</code></pre>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
// eslint-disable-next-line simple-import-sort/imports
|
|
10
|
+
import { defineComponent, onMounted, ref } from "vue";
|
|
11
|
+
|
|
12
|
+
import Prism from "prismjs";
|
|
13
|
+
|
|
14
|
+
import "prismjs/components/prism-yaml";
|
|
15
|
+
import "prismjs/plugins/custom-class/prism-custom-class";
|
|
16
|
+
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
|
|
17
|
+
import "prismjs/plugins/line-numbers/prism-line-numbers";
|
|
18
|
+
import "prismjs/themes/prism-twilight.css";
|
|
19
|
+
|
|
20
|
+
export default defineComponent({
|
|
21
|
+
name: "YAML",
|
|
22
|
+
props: {
|
|
23
|
+
yaml: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
setup() {
|
|
29
|
+
const pre = ref<HTMLElement | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
Prism.plugins.customClass.map({
|
|
32
|
+
number: "prism-number",
|
|
33
|
+
tag: "prism-tag",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
onMounted(() => {
|
|
37
|
+
if (pre.value) {
|
|
38
|
+
pre.value.querySelectorAll("code").forEach((elem) => {
|
|
39
|
+
Prism.highlightElement(elem);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { pre };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="control" v-if="!isDeleted">
|
|
3
|
+
<div
|
|
4
|
+
class="tags has-addons are-medium"
|
|
5
|
+
v-on:mouseover="showDeleteButton"
|
|
6
|
+
v-on:mouseleave="hideDeleteButton"
|
|
7
|
+
>
|
|
8
|
+
<span class="tag is-info is-light" @click="updateTag">{{
|
|
9
|
+
tag.name
|
|
10
|
+
}}</span>
|
|
11
|
+
<a
|
|
12
|
+
class="tag is-delete"
|
|
13
|
+
v-if="isDeleteButtonEnabled"
|
|
14
|
+
@click="deleteTag"
|
|
15
|
+
></a>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { defineComponent, PropType, ref } from "vue";
|
|
22
|
+
|
|
23
|
+
import { generateDeleteTagTask } from "@/api-helper";
|
|
24
|
+
import { Tag } from "@/types";
|
|
25
|
+
|
|
26
|
+
export default defineComponent({
|
|
27
|
+
name: "TagItem",
|
|
28
|
+
props: {
|
|
29
|
+
tag: {
|
|
30
|
+
type: Object as PropType<Tag>,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
setup(props, context) {
|
|
35
|
+
const isDeleted = ref(false);
|
|
36
|
+
const isDeleteButtonEnabled = ref(false);
|
|
37
|
+
|
|
38
|
+
const deleteTagTask = generateDeleteTagTask();
|
|
39
|
+
|
|
40
|
+
const deleteTag = async () => {
|
|
41
|
+
const result = window.confirm(
|
|
42
|
+
`Are you sure you want to delete ${props.tag.name}?`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (result) {
|
|
46
|
+
await deleteTagTask.perform(props.tag.name);
|
|
47
|
+
isDeleted.value = true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const showDeleteButton = () => {
|
|
52
|
+
isDeleteButtonEnabled.value = true;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const hideDeleteButton = () => {
|
|
56
|
+
isDeleteButtonEnabled.value = false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const updateTag = () => {
|
|
60
|
+
context.emit("update-tag", props.tag.name);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
updateTag,
|
|
65
|
+
isDeleted,
|
|
66
|
+
deleteTag,
|
|
67
|
+
showDeleteButton,
|
|
68
|
+
hideDeleteButton,
|
|
69
|
+
isDeleteButtonEnabled,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="field is-grouped is-grouped-multiline">
|
|
3
|
+
<TagComponent
|
|
4
|
+
v-for="tag in tags"
|
|
5
|
+
:tag="tag"
|
|
6
|
+
:key="tag.name"
|
|
7
|
+
@update-tag="updateTag"
|
|
8
|
+
></TagComponent>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { defineComponent, PropType } from "vue";
|
|
14
|
+
|
|
15
|
+
import TagComponent from "@/components/tag/Tag.vue";
|
|
16
|
+
import { Tag } from "@/types";
|
|
17
|
+
|
|
18
|
+
export default defineComponent({
|
|
19
|
+
name: "TagsItem",
|
|
20
|
+
components: {
|
|
21
|
+
TagComponent,
|
|
22
|
+
},
|
|
23
|
+
props: {
|
|
24
|
+
tags: {
|
|
25
|
+
type: Array as PropType<Tag[]>,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
setup(_, context) {
|
|
30
|
+
const updateTag = (tag: string) => {
|
|
31
|
+
context.emit("update-tag", tag);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return { updateTag };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
</script>
|