web/satellite: second step of new access grant flow

Added second step where user sets permissions, buckets and date caveats.

Change-Id: I2f959a5bbd46b47b52ea9cafc866b13d28b3e972
This commit is contained in:
Vitalii 2023-02-13 18:25:31 +02:00 committed by Storj Robot
parent 155b927c5e
commit 9ac314e482
12 changed files with 850 additions and 9 deletions

View File

@ -15,7 +15,22 @@
:selected-access-types="selectedAccessTypes"
:name="accessName"
:set-name="setAccessName"
:on-continue="setPermissionsStep"
:on-continue="() => setStep(CreateAccessStep.ChoosePermission)"
/>
<ChoosePermissionStep
v-if="step === CreateAccessStep.ChoosePermission"
:on-select-permission="selectPermissions"
:selected-permissions="selectedPermissions"
:on-back="() => setStep(CreateAccessStep.CreateNewAccess)"
:on-continue="() => setStep(CreateAccessStep.AccessEncryption)"
:selected-buckets="selectedBuckets"
:on-select-bucket="selectBucket"
:on-select-all-buckets="selectAllBuckets"
:on-unselect-bucket="unselectBucket"
:not-after="notAfter"
:on-set-not-after="setNotAfter"
:not-after-label="notAfterLabel"
:on-set-not-after-label="setNotAfterLabel"
/>
</div>
</template>
@ -25,19 +40,35 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from '@/utils/hooks';
import { useNotify, useRoute, useRouter, useStore } from '@/utils/hooks';
import { RouteConfig } from '@/router';
import { AccessType, CreateAccessStep, STEP_ICON_AND_TITLE } from '@/types/createAccessGrant';
import { AccessType, CreateAccessStep, Permission, STEP_ICON_AND_TITLE } from '@/types/createAccessGrant';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import VModal from '@/components/common/VModal.vue';
import CreateNewAccessStep from '@/components/accessGrants/newCreateFlow/steps/CreateNewAccessStep.vue';
import ChoosePermissionStep from '@/components/accessGrants/newCreateFlow/steps/ChoosePermissionStep.vue';
const router = useRouter();
const route = useRoute();
const notify = useNotify();
const store = useStore();
const initPermissions = [
Permission.Read,
Permission.Write,
Permission.Delete,
Permission.List,
];
const step = ref<CreateAccessStep>(CreateAccessStep.CreateNewAccess);
const selectedAccessTypes = ref<AccessType[]>([]);
const selectedPermissions = ref<Permission[]>(initPermissions);
const selectedBuckets = ref<string[]>([]);
const accessName = ref<string>('');
const notAfter = ref<Date | undefined>(undefined);
const notAfterLabel = ref<string>('No end date');
/**
* Selects access type.
@ -89,6 +120,20 @@ function selectAccessType(type: AccessType) {
}
}
/**
* Sets not after (end date) caveat.
*/
function setNotAfter(date: Date | undefined): void {
notAfter.value = date;
}
/**
* Sets not after (end date) label.
*/
function setNotAfterLabel(label: string): void {
notAfterLabel.value = label;
}
/**
* Unselects API key access type.
*/
@ -98,6 +143,66 @@ function unselectAPIKeyAccessType(): void {
}
}
/**
* Selects access grant permissions.
*/
function selectPermissions(permission: Permission): void {
switch (permission) {
case Permission.All:
if (selectedPermissions.value.length === 4) {
selectedPermissions.value = [];
return;
}
selectedPermissions.value = initPermissions;
break;
case Permission.Delete:
handlePermissionSelection(Permission.Delete);
break;
case Permission.List:
handlePermissionSelection(Permission.List);
break;
case Permission.Write:
handlePermissionSelection(Permission.Write);
break;
case Permission.Read:
handlePermissionSelection(Permission.Read);
}
}
/**
* Handles permission select/unselect.
*/
function handlePermissionSelection(permission: Permission) {
if (selectedPermissions.value.includes(permission)) {
selectedPermissions.value = selectedPermissions.value.filter(p => p !== permission);
return;
}
selectedPermissions.value.push(permission);
}
/**
* Clears bucket selection which means grant access to all buckets.
*/
function selectAllBuckets() {
selectedBuckets.value = [];
}
/**
* Select some specific bucket.
*/
function selectBucket(bucket: string) {
selectedBuckets.value.push(bucket);
}
/**
* Unselect some specific bucket.
*/
function unselectBucket(bucket: string) {
selectedBuckets.value = selectedBuckets.value.filter(b => b !== bucket);
}
/**
* Sets access grant name from input field.
* @param value
@ -109,8 +214,8 @@ function setAccessName(value: string): void {
/**
* Sets current step to be 'Choose permission'.
*/
function setPermissionsStep(): void {
step.value = CreateAccessStep.ChoosePermission;
function setStep(stepArg: CreateAccessStep): void {
step.value = stepArg;
}
/**
@ -120,10 +225,16 @@ function closeModal(): void {
router.push(RouteConfig.AccessGrants.path);
}
onMounted(() => {
onMounted(async () => {
if (route.params?.accessType) {
selectedAccessTypes.value.push(route.params?.accessType as AccessType);
}
try {
await store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
} catch (error) {
notify.error(`Unable to fetch all bucket names. ${error.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL);
}
});
</script>

View File

@ -32,7 +32,7 @@ const props = defineProps<{
&__functional {
margin-left: 16px;
width: 100%;
width: calc(100% - 56px);
&__title {
font-family: 'font_bold', sans-serif;

View File

@ -0,0 +1,159 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div v-click-outside="closePicker" class="date-picker">
<ul class="date-picker__column">
<li class="date-picker__column__item" @click="onOneDayClick">24 Hours</li>
<li class="date-picker__column__item" @click="onOneWeekClick">1 Week</li>
<li class="date-picker__column__item" @click="onOneMonthClick">1 month</li>
<li class="date-picker__column__item" @click="onSixMonthsClick">6 Months</li>
<li class="date-picker__column__item" @click="onOneYearClick">1 Year</li>
<li class="date-picker__column__item" @click="onForeverClick">No end date</li>
</ul>
<VDatePicker :on-date-pick="onCustomDatePick" />
</div>
</template>
<script setup lang="ts">
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { useStore } from '@/utils/hooks';
import VDatePicker from '@/components/common/VDatePicker.vue';
const props = defineProps<{
setLabel: (label: string) => void;
setNotAfter: (date: Date | undefined) => void;
}>();
const store = useStore();
/**
* Closes date picker.
*/
function closePicker(): void {
store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
}
/**
* onCustomDatePick holds logic for choosing custom date.
* @param date
*/
function onCustomDatePick(date: Date): void {
const to = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59);
const toFormattedString = to.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
props.setLabel(toFormattedString);
props.setNotAfter(to);
closePicker();
}
/**
* Holds on "No end date" choice click logic.
*/
function onForeverClick(): void {
props.setLabel('No end date');
props.setNotAfter(undefined);
closePicker();
}
/**
* Holds on "1 month" choice click logic.
*/
function onOneMonthClick(): void {
const now = new Date();
const inAMonth = new Date(now.setMonth(now.getMonth() + 1));
props.setLabel('1 Month');
props.setNotAfter(inAMonth);
closePicker();
}
/**
* Holds on "24 hours" choice click logic.
*/
function onOneDayClick(): void {
const now = new Date();
const inADay = new Date(now.setDate(now.getDate() + 1));
props.setLabel('24 Hours');
props.setNotAfter(inADay);
closePicker();
}
/**
* Holds on "1 week" choice click logic.
*/
function onOneWeekClick(): void {
const now = new Date();
const inAWeek = new Date(now.setDate(now.getDate() + 7));
props.setLabel('1 Week');
props.setNotAfter(inAWeek);
closePicker();
}
/**
* Holds on "6 month" choice click logic.
*/
function onSixMonthsClick(): void {
const now = new Date();
const inSixMonth = new Date(now.setMonth(now.getMonth() + 6));
props.setLabel('6 Months');
props.setNotAfter(inSixMonth);
closePicker();
}
/**
* Holds on "1 year" choice click logic.
*/
function onOneYearClick(): void {
const now = new Date();
const inOneYear = new Date(now.setFullYear(now.getFullYear() + 1));
props.setLabel('1 Year');
props.setNotAfter(inOneYear);
closePicker();
}
</script>
<style scoped lang="scss">
.date-picker {
background: #fff;
width: 410px;
border: 1px solid #384b65;
border-radius: 6px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
position: absolute;
z-index: 1;
top: 100%;
left: 0;
display: flex;
align-items: center;
cursor: default;
&__column {
list-style-type: none;
padding-left: 0;
margin-top: 0;
&__item {
font-size: 14px;
font-weight: 400;
padding: 10px 12px;
color: #1b2533;
cursor: pointer;
white-space: nowrap;
&:hover {
font-weight: bold;
background: #f5f6fa;
}
}
}
&__wrapper {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,82 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="date-select">
<div
class="date-select__toggle-container"
aria-roledescription="select-date"
@click.stop="togglePicker"
>
<h1 class="date-select__toggle-container__label">{{ notAfterLabel }}</h1>
<ExpandIcon />
</div>
<EndDatePicker
v-if="isDatePickerVisible"
:set-label="setNotAfterLabel"
:set-not-after="setNotAfter"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import EndDatePicker from './EndDatePicker.vue';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { useStore } from '@/utils/hooks';
import { APP_STATE_DROPDOWNS } from '@/utils/constants/appStatePopUps';
import ExpandIcon from '@/../static/images/common/BlackArrowExpand.svg';
const props = defineProps<{
setNotAfter: (date: Date | undefined) => void;
setNotAfterLabel: (label: string) => void;
notAfterLabel: string;
}>();
const store = useStore();
/**
* Indicates if date picker is shown.
*/
const isDatePickerVisible = computed((): boolean => {
return store.state.appStateModule.appState.activeDropdown == APP_STATE_DROPDOWNS.AG_DATE_PICKER;
});
/**
* Toggles date picker.
*/
function togglePicker(): void {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_ACTIVE_DROPDOWN, APP_STATE_DROPDOWNS.AG_DATE_PICKER);
}
</script>
<style scoped lang="scss">
.date-select {
background-color: #fff;
cursor: pointer;
border-radius: 6px;
border: 1px solid rgb(56 75 101 / 40%);
font-family: 'font_regular', sans-serif;
position: relative;
box-sizing: border-box;
width: 100%;
&__toggle-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
width: calc(100% - 32px);
&__label {
font-size: 16px;
line-height: 21px;
color: #384b65;
margin: 0;
}
}
}
</style>

View File

@ -4,10 +4,13 @@
<template>
<div class="toggle">
<label class="toggle__input-container">
<input :id="`checkbox${label}`" :checked="checked" type="checkbox" @change="onCheck">
<input :id="id || label" :checked="checked" type="checkbox" @change="onCheck">
<span />
</label>
<label class="toggle__label" :for="`checkbox${label}`">{{ label }}</label>
<label class="toggle__label" :for="id || label">{{ label }}</label>
<template v-if="onShowHideAll">
<ChevronIcon class="toggle__chevron" :class="{'toggle__chevron--up': allShown}" @click="onShowHideAll" />
</template>
<VInfo v-if="slots.infoMessage" class="toggle__info">
<template #icon>
<InfoIcon class="toggle__info__icon" />
@ -25,6 +28,7 @@ import { useSlots } from 'vue';
import VInfo from '@/components/common/VInfo.vue';
import InfoIcon from '@/../static/images/accessGrants/newCreateFlow/info.svg';
import ChevronIcon from '@/../static/images/accessGrants/newCreateFlow/chevron.svg';
const slots = useSlots();
@ -32,10 +36,15 @@ const props = withDefaults(defineProps<{
checked: boolean;
label: string;
onCheck: () => void;
id?: string;
onShowHideAll?: () => void;
allShown?: boolean;
}>(), {
checked: false,
label: '',
id: '',
onCheck: () => {},
onShowHideAll: undefined,
});
</script>
@ -118,6 +127,15 @@ const props = withDefaults(defineProps<{
cursor: pointer;
}
}
&__chevron {
transition: transform 0.3s;
margin-left: 8px;
&--up {
transform: rotate(180deg);
}
}
}
:deep(.info__box) {

View File

@ -0,0 +1,342 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="choose">
<ContainerWithIcon :icon-and-title="FUNCTIONAL_CONTAINER_ICON_AND_TITLE[FunctionalContainer.Permissions]">
<template #functional>
<div class="choose__toggles">
<Toggle
id="all permissions"
:checked="selectedPermissions.length === 4"
label="All"
:on-check="() => onSelectPermission(Permission.All)"
:on-show-hide-all="togglePermissionsVisibility"
:all-shown="allPermissionsShown"
/>
<template v-if="allPermissionsShown">
<Toggle
:checked="selectedPermissions.includes(Permission.Read)"
label="Read"
:on-check="() => onSelectPermission(Permission.Read)"
/>
<Toggle
:checked="selectedPermissions.includes(Permission.Write)"
label="Write"
:on-check="() => onSelectPermission(Permission.Write)"
/>
<Toggle
:checked="selectedPermissions.includes(Permission.List)"
label="List"
:on-check="() => onSelectPermission(Permission.List)"
/>
<Toggle
:checked="selectedPermissions.includes(Permission.Delete)"
label="Delete"
:on-check="() => onSelectPermission(Permission.Delete)"
/>
</template>
</div>
</template>
</ContainerWithIcon>
<ContainerWithIcon :icon-and-title="FUNCTIONAL_CONTAINER_ICON_AND_TITLE[FunctionalContainer.Buckets]">
<template #functional>
<Toggle
id="all buckets"
:checked="selectedBuckets.length === 0"
label="All"
:on-check="selectAllBuckets"
:on-show-hide-all="toggleBucketsVisibility"
:all-shown="searchBucketsShown"
/>
<template v-if="searchBucketsShown">
<div v-if="selectedBuckets.length" class="choose__selected-container">
<div v-for="bucket in selectedBuckets" :key="bucket" class="choose__selected-container__item">
<p class="choose__selected-container__item__label">{{ bucket }}</p>
<CloseIcon @click="() => onUnselectBucket(bucket)" />
</div>
</div>
<div class="choose__search-container">
<SearchIcon />
<input v-model="searchQuery" placeholder="Search by name">
</div>
<div v-if="searchQuery" class="choose__bucket-results">
<template v-if="bucketsList.length">
<p
v-for="bucket in bucketsList"
:key="bucket"
class="choose__bucket-results__item"
@click="() => selectBucket(bucket)"
>
{{ bucket }}
</p>
</template>
<template v-else>
<p class="choose__bucket-results__empty">No Buckets found.</p>
</template>
</div>
</template>
</template>
</ContainerWithIcon>
<ContainerWithIcon :icon-and-title="FUNCTIONAL_CONTAINER_ICON_AND_TITLE[FunctionalContainer.EndDate]">
<template #functional>
<div class="choose__date-selection">
<p v-if="!settingDate && !notAfter" class="choose__date-selection__label" @click="toggleSettingDate">
Add Date (optional)
</p>
<EndDateSelection
v-else
:set-not-after="onSetNotAfter"
:not-after-label="notAfterLabel"
:set-not-after-label="onSetNotAfterLabel"
/>
</div>
</template>
</ContainerWithIcon>
<ButtonsContainer>
<template #leftButton>
<VButton
label="Back"
width="100%"
height="48px"
font-size="14px"
:on-press="onBack"
:is-white="true"
/>
</template>
<template #rightButton>
<VButton
label="Continue ->"
width="100%"
height="48px"
font-size="14px"
:on-press="onContinue"
:is-disabled="isButtonDisabled"
/>
</template>
</ButtonsContainer>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
FUNCTIONAL_CONTAINER_ICON_AND_TITLE,
FunctionalContainer,
Permission,
} from '@/types/createAccessGrant';
import { useStore } from '@/utils/hooks';
import ContainerWithIcon from '@/components/accessGrants/newCreateFlow/components/ContainerWithIcon.vue';
import ButtonsContainer from '@/components/accessGrants/newCreateFlow/components/ButtonsContainer.vue';
import EndDateSelection from '@/components/accessGrants/newCreateFlow/components/EndDateSelection.vue';
import Toggle from '@/components/accessGrants/newCreateFlow/components/Toggle.vue';
import VButton from '@/components/common/VButton.vue';
import SearchIcon from '@/../static/images/accessGrants/newCreateFlow/search.svg';
import CloseIcon from '@/../static/images/accessGrants/newCreateFlow/close.svg';
const props = withDefaults(defineProps<{
selectedPermissions: Permission[];
onSelectPermission: (type: Permission) => void;
selectedBuckets: string[];
onSelectBucket: (bucket: string) => void;
onUnselectBucket: (bucket: string) => void;
onSelectAllBuckets: () => void;
onSetNotAfter: (date: Date | undefined) => void;
onSetNotAfterLabel: (label: string) => void;
notAfterLabel: string;
onBack: () => void;
onContinue: () => void;
notAfter?: Date;
}>(), {
notAfter: undefined,
});
const store = useStore();
const allPermissionsShown = ref<boolean>(false);
const searchBucketsShown = ref<boolean>(false);
const settingDate = ref<boolean>(false);
const searchQuery = ref<string>('');
/**
* Indicates if button should be disabled.
*/
const isButtonDisabled = computed((): boolean => {
return !props.selectedPermissions.length;
});
/**
* Returns stored bucket names list filtered by search string.
*/
const bucketsList = computed((): string[] => {
const NON_EXIST_INDEX = -1;
const buckets: string[] = store.state.bucketUsageModule.allBucketNames;
return buckets.filter((name: string) => {
return name.indexOf(searchQuery.value.toLowerCase()) !== NON_EXIST_INDEX && !props.selectedBuckets.includes(name);
});
});
/**
* Selects bucket and clears search.
*/
function selectBucket(bucket: string): void {
props.onSelectBucket(bucket);
searchQuery.value = '';
}
/**
* Selects all buckets and clears search.
*/
function selectAllBuckets(): void {
props.onSelectAllBuckets();
searchQuery.value = '';
}
/**
* Toggles end date selection visibility.
*/
function toggleSettingDate(): void {
settingDate.value = true;
}
/**
* Toggles full list of permissions visibility.
*/
function togglePermissionsVisibility(): void {
allPermissionsShown.value = !allPermissionsShown.value;
}
/**
* Toggles bucket search/select visibility.
*/
function toggleBucketsVisibility(): void {
searchBucketsShown.value = !searchBucketsShown.value;
}
</script>
<style lang="scss" scoped>
.choose {
font-family: 'font_regular', sans-serif;
&__toggles {
display: flex;
flex-direction: column;
row-gap: 16px;
}
&__search-container {
display: flex;
align-items: center;
background: #fff;
border: 1px solid #d8dee3;
border-radius: 8px;
padding: 12px;
margin-top: 16px;
svg {
min-width: 17px;
margin-right: 12px;
}
input {
font-size: 14px;
line-height: 20px;
color: #000;
border: none;
outline: none;
}
}
&__selected-container {
flex-wrap: wrap;
display: flex;
align-items: center;
margin-top: 16px;
column-gap: 4px;
row-gap: 4px;
&__item {
max-width: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 6px 16px;
border: 1px solid #d8dee3;
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
border-radius: 8px;
&__label {
margin-right: 8px;
font-family: 'font_bold', sans-serif;
font-size: 12px;
line-height: 20px;
color: #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
svg {
cursor: pointer;
min-width: 14px;
}
}
&__bucket-results {
padding: 4px 0;
max-height: 120px;
overflow-y: auto;
border: 1px solid #d8dee3;
box-shadow: 0 4px 6px -2px rgb(0 0 0 / 5%);
border-radius: 6px;
max-width: 100%;
box-sizing: border-box;
&__item {
background-color: #fff;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #000;
padding: 10px 16px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
cursor: pointer;
&:hover {
background-color: #ecedf2;
}
}
&__empty {
padding: 10px 16px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #000;
text-align: left;
}
}
&__date-selection {
margin-top: -8px;
&__label {
font-size: 14px;
line-height: 22px;
text-decoration: underline;
text-underline-position: under;
color: #56606d;
text-align: left;
cursor: pointer;
}
}
}
</style>

View File

@ -65,6 +65,7 @@
<VInput
class="create__input"
placeholder="Input Access Name"
:init-value="name"
@setData="setName"
/>
</template>

View File

@ -0,0 +1,110 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<DatePicker
aria-roledescription="datepicker"
:open="true"
:inline="true"
:value="date"
popup-class="picker"
@change="onDatePick"
/>
</template>
<script setup lang="ts">
import DatePicker from 'vue2-datepicker';
const props = withDefaults(defineProps<{
onDatePick?: (date: Date) => void;
date?: Date;
}>(), {
onDatePick: () => () => false,
date: () => new Date(),
});
</script>
<style scoped lang="scss">
@import '~vue2-datepicker/scss/index';
.picker,
.mx-datepicker {
width: 100%;
cursor: default;
}
.mx-calendar {
width: 100%;
}
.mx-date-row {
height: 40px;
}
.mx-table th {
color: var(--c-grey-4);
}
.mx-table-date .cell.not-current-month {
color: var(--c-grey-5);
}
.mx-calendar-content .cell {
font-size: 12px;
line-height: 18px;
color: #000;
}
.mx-calendar-content .cell.in-range {
color: #000;
background-color: var(--c-blue-1);
border-radius: 999px;
}
.mx-calendar-content .cell.active {
background-color: var(--c-blue-3);
border-radius: 999px;
}
.mx-calendar-content {
height: unset;
}
.mx-btn-current-month,
.mx-btn-current-year {
pointer-events: none;
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 20px;
color: #000;
}
.mx-btn:hover {
border-color: #000;
color: #000;
}
.mx-btn-icon-double-right,
.mx-btn-icon-double-left {
display: none;
}
.mx-icon-left:before,
.mx-icon-right:before {
width: 20px;
height: 20px;
border-width: 4px 0 0 4px;
}
@media screen and (max-width: 768px) {
.mx-range-wrapper {
flex-direction: column;
}
.mx-calendar + .mx-calendar {
border-left: none;
border-top: 1px solid #e8e8e8;
}
}
</style>

View File

@ -39,6 +39,14 @@ export enum CreateAccessStep {
CredentialsCreated = 'credentialsCreated',
}
export enum Permission {
All = 'all',
Read = 'read',
Write = 'write',
List = 'list',
Delete = 'delete',
}
export const STEP_ICON_AND_TITLE: Record<CreateAccessStep, IconAndTitle> = {
[CreateAccessStep.CreateNewAccess]: {
icon: CreateNewAccessIcon,

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.28014 11.4806C8.13058 11.6302 7.89117 11.6352 7.73562 11.4956L7.71983 11.4806L0.916032 4.67685C0.761306 4.52212 0.761306 4.27126 0.916032 4.11653C1.0656 3.96696 1.305 3.96198 1.46056 4.10158L1.47635 4.11653L7.99998 10.6402L14.5236 4.11653C14.6732 3.96696 14.9126 3.96198 15.0682 4.10158L15.0839 4.11653C15.2335 4.2661 15.2385 4.5055 15.0989 4.66106L15.0839 4.67685L8.28014 11.4806Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.4547 2.54591C13.915 5.00621 13.915 8.99515 11.4547 11.4555C8.99442 13.9158 5.00548 13.9158 2.54518 11.4555C0.0848754 8.99515 0.0848754 5.00621 2.54518 2.54591C5.00548 0.0856078 8.99442 0.0856078 11.4547 2.54591ZM10.638 3.36262C8.62877 1.35337 5.37113 1.35337 3.36189 3.36262C1.35264 5.37187 1.35264 8.6295 3.36189 10.6387C5.37113 12.648 8.62877 12.648 10.638 10.6387C12.6473 8.6295 12.6473 5.37187 10.638 3.36262ZM8.90593 5.83673L8.89952 5.84347L7.7557 6.98725L8.86583 8.09737C9.09365 8.32519 9.09976 8.69263 8.87964 8.92789C8.66875 9.1533 8.31506 9.16507 8.08966 8.95418L8.08292 8.94777L6.939 7.80395L5.80162 8.94148C5.57609 9.16701 5.21044 9.16701 4.98491 8.94148C4.76488 8.72145 4.75952 8.36805 4.96881 8.14152L4.98491 8.12477L6.12229 6.98725L4.98491 5.84987C4.75938 5.62434 4.75938 5.25869 4.98491 5.03316C5.20494 4.81313 5.55834 4.80777 5.78487 5.01706L5.80162 5.03316L6.939 6.17054L8.04912 5.06056C8.27694 4.83274 8.64438 4.82663 8.87964 5.04675C9.10505 5.25764 9.11682 5.61133 8.90593 5.83673Z" fill="#929FB1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.67582 12.7785C10.6736 12.7785 13.1037 10.3907 13.1037 7.44515C13.1037 4.49963 10.6736 2.11182 7.67582 2.11182C4.67808 2.11182 2.24792 4.49963 2.24792 7.44515C2.24792 10.3907 4.67808 12.7785 7.67582 12.7785Z" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.4606 14.1109L11.5092 11.2109" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 552 B